SHA256
13 01 25
Переписал код кучи классов перешёл на новый надеюсь теперь подходящий формат блоков и тесты переделал. Но пока остались баги и тесты не проходят (в частности пользователи не создаются - ошибка в бд)
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
// =======================
|
||||
// blockchain/BchBlockEntry.java (НОВАЯ ВЕРСИЯ под ТЗ)
|
||||
// =======================
|
||||
package blockchain;
|
||||
|
||||
import blockchain.body.BodyRecord;
|
||||
@@ -9,100 +12,66 @@ import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* старый формат -его надо поменять на новый формат
|
||||
*
|
||||
* RAW (BigEndian):
|
||||
* [4] recordSize (int) = размер RAW (включая этот заголовок), БЕЗ signature+hash
|
||||
* [4] recordNumber (int) глобальный номер блока
|
||||
* [8] timestamp (long) unix seconds
|
||||
[2] lineIndex (short)
|
||||
* [4] lineNumber (int)
|
||||
* [N] bodyBytes (body, начинается с [type][version])
|
||||
*
|
||||
* TAIL (НЕ входит в recordSize):
|
||||
* [64] signature64 (Ed25519)
|
||||
* [32] hash32 (SHA-256)
|
||||
*/
|
||||
|
||||
/**
|
||||
* BchBlockEntry — универсальный блок нового формата.
|
||||
*
|
||||
* RAW (BigEndian):
|
||||
* Неизменное заглавие
|
||||
* [32] prevHash32 (SHA-256) ХЭЩ ПРИВЕДУЩЕГО
|
||||
* [4] blockSize (int) = размер RAW (включая этот заголовок), БЕЗ signature
|
||||
* [4] blockNumber (int) номер блока
|
||||
* [8] timestamp (long) unix seconds
|
||||
|
||||
* RAW (BigEndian) = preimage:
|
||||
* [32] prevHash32 (SHA-256) hash предыдущего блока (цепочка)
|
||||
* [4] blockSize (int) = размер preimage (в байтах), БЕЗ signature64
|
||||
* [4] blockNumber (int) глобальный номер блока
|
||||
* [8] timestamp (long) unix seconds
|
||||
*
|
||||
* [2] type - тип соощения
|
||||
* [2] Sиbtype - субтип сообщения
|
||||
* [2] version - версия формата соощения
|
||||
* [2] type (short) тип сообщения
|
||||
* [2] subType (short) подтип сообщения
|
||||
* [2] version (short) версия формата сообщения
|
||||
*
|
||||
* [N] bodyBytes (bytes) тело сообщения (БЕЗ type/subType/version)
|
||||
*
|
||||
* Дальше Само сообщение (может быть разным)
|
||||
* [4] prevLineNumber НОМЕР ПРИВЕДУЩЕГО СООБЩЕНИЯ В ЛИНИИ - может быть а может и небыть в зависимости от типа сообщения
|
||||
* [32] prevLineHash ХЭШ ПРИВЕДУЩЕГО СООБЩЕНИЯ В ЛИНИИ - может быть а может и небыть в зависимости от типа сообщения
|
||||
* [4] номер самого сообщения в этой линии
|
||||
* [N] bodyBytes (ОСТАЛЬНЫЕ БАЙТЫ])
|
||||
|
||||
* TAIL (НЕ входит в recordSize):
|
||||
* [64] signature64 (Ed25519)
|
||||
* И хэш в конце блока мы не храним, тк он будет в начале следующего блока. А для проверки блока оно не нужно тк мы каждый раз провеяем подпись . А она основана на хэше
|
||||
* TAIL (НЕ входит в blockSize):
|
||||
* [64] signature64 (Ed25519) подпись над hash32
|
||||
*
|
||||
|
||||
|
||||
* [32] hash32 (SHA-256)
|
||||
* hash32 ВНУТРИ БЛОКА НЕ ХРАНИМ.
|
||||
* hash32 вычисляется при парсинге:
|
||||
* preimage = первые blockSize байт
|
||||
* hash32 = SHA-256(preimage)
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public final class BchBlockEntry {
|
||||
|
||||
public static final int SIGNATURE_LEN = 64;
|
||||
public static final int HASH_LEN = 32;
|
||||
|
||||
/** Размер фиксированного RAW-заголовка без body */
|
||||
public static final int RAW_HEADER_SIZE = 4 + 4 + 8 + 2 + 4;
|
||||
public static final int RAW_HEADER_SIZE =
|
||||
32 // prevHash32
|
||||
+ 4 // blockSize
|
||||
+ 4 // blockNumber
|
||||
+ 8 // timestamp
|
||||
+ 2 // type
|
||||
+ 2 // subType
|
||||
+ 2; // version
|
||||
|
||||
// --- RAW ---
|
||||
public final int recordSize; // только RAW, без signature+hash
|
||||
public final int recordNumber;
|
||||
// --- HEADER (RAW) ---
|
||||
public final byte[] prevHash32; // 32
|
||||
public final int blockSize; // preimage size
|
||||
public final int blockNumber;
|
||||
public final long timestamp;
|
||||
public final short lineIndex;
|
||||
public final int lineNumber;
|
||||
public final short type;
|
||||
public final short subType;
|
||||
public final short version;
|
||||
|
||||
// --- BODY (RAW) ---
|
||||
public final byte[] bodyBytes;
|
||||
|
||||
/** Распарсенное тело (создаётся сразу при парсинге блока). */
|
||||
public final BodyRecord body;
|
||||
|
||||
// --- TAIL ---
|
||||
private final byte[] signature64;
|
||||
private final byte[] hash32;
|
||||
private final byte[] signature64; // 64
|
||||
|
||||
// --- cached ---
|
||||
private final byte[] fullBytes;
|
||||
// --- derived ---
|
||||
private final byte[] hash32; // 32, computed
|
||||
private final byte[] preimage; // blockSize bytes
|
||||
private final byte[] fullBytes; // preimage + signature
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор из байт ========================== */
|
||||
@@ -110,113 +79,113 @@ public final class BchBlockEntry {
|
||||
|
||||
public BchBlockEntry(byte[] fullBytes) {
|
||||
Objects.requireNonNull(fullBytes, "fullBytes == null");
|
||||
if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN + HASH_LEN)
|
||||
|
||||
if (fullBytes.length < RAW_HEADER_SIZE + SIGNATURE_LEN) {
|
||||
throw new IllegalArgumentException("Block too short");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(fullBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
this.recordSize = bb.getInt();
|
||||
if (recordSize + SIGNATURE_LEN + HASH_LEN != fullBytes.length)
|
||||
throw new IllegalArgumentException("recordSize mismatch");
|
||||
this.prevHash32 = new byte[32];
|
||||
bb.get(this.prevHash32);
|
||||
|
||||
this.recordNumber = bb.getInt();
|
||||
this.blockSize = bb.getInt();
|
||||
if (blockSize < RAW_HEADER_SIZE) {
|
||||
throw new IllegalArgumentException("blockSize too small: " + blockSize);
|
||||
}
|
||||
if (blockSize + SIGNATURE_LEN != fullBytes.length) {
|
||||
throw new IllegalArgumentException("blockSize mismatch: blockSize=" + blockSize + " fullLen=" + fullBytes.length);
|
||||
}
|
||||
|
||||
this.blockNumber = bb.getInt();
|
||||
this.timestamp = bb.getLong();
|
||||
this.lineIndex = bb.getShort();
|
||||
this.lineNumber = bb.getInt();
|
||||
|
||||
int bodyLen = recordSize - RAW_HEADER_SIZE;
|
||||
if (bodyLen <= 0)
|
||||
throw new IllegalArgumentException("Invalid body length");
|
||||
this.type = bb.getShort();
|
||||
this.subType = bb.getShort();
|
||||
this.version = bb.getShort();
|
||||
|
||||
int bodyLen = blockSize - RAW_HEADER_SIZE;
|
||||
if (bodyLen < 0) throw new IllegalArgumentException("Invalid body length: " + bodyLen);
|
||||
|
||||
this.bodyBytes = new byte[bodyLen];
|
||||
bb.get(this.bodyBytes);
|
||||
|
||||
// ✅ Сразу парсим BodyRecord (и если неизвестный type/version — тут же упадём)
|
||||
this.body = BodyRecordParser.parse(this.bodyBytes);
|
||||
|
||||
// ✅ УРОВЕНЬ B: проверка ожидаемой линии по типу body
|
||||
short expectedLine = this.body.expectedLineIndex();
|
||||
if (this.lineIndex != expectedLine) {
|
||||
throw new IllegalArgumentException(
|
||||
"Body is in wrong lineIndex: expected=" + expectedLine + " actual=" + this.lineIndex +
|
||||
" (type=" + this.body.type() + " ver=" + this.body.version() + ")"
|
||||
);
|
||||
}
|
||||
|
||||
this.signature64 = new byte[SIGNATURE_LEN];
|
||||
bb.get(this.signature64);
|
||||
|
||||
this.hash32 = new byte[HASH_LEN];
|
||||
bb.get(this.hash32);
|
||||
// preimage = первые blockSize байт
|
||||
this.preimage = Arrays.copyOfRange(fullBytes, 0, blockSize);
|
||||
|
||||
// hash32 = sha256(preimage)
|
||||
this.hash32 = BchCryptoVerifier.sha256(preimage);
|
||||
|
||||
// parse body по header.type/subType/version
|
||||
this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
|
||||
|
||||
this.fullBytes = Arrays.copyOf(fullBytes, fullBytes.length);
|
||||
|
||||
// запрет мусора
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор сборки ============================ */
|
||||
/* ===================================================================== */
|
||||
|
||||
public BchBlockEntry(int recordNumber,
|
||||
public BchBlockEntry(byte[] prevHash32,
|
||||
int blockNumber,
|
||||
long timestamp,
|
||||
short lineIndex,
|
||||
int lineNumber,
|
||||
short type,
|
||||
short subType,
|
||||
short version,
|
||||
byte[] bodyBytes,
|
||||
byte[] signature64,
|
||||
byte[] hash32) {
|
||||
byte[] signature64) {
|
||||
|
||||
Objects.requireNonNull(prevHash32, "prevHash32 == null");
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
Objects.requireNonNull(signature64, "signature64 == null");
|
||||
Objects.requireNonNull(hash32, "hash32 == null");
|
||||
|
||||
if (signature64.length != SIGNATURE_LEN)
|
||||
throw new IllegalArgumentException("signature64 != 64");
|
||||
if (hash32.length != HASH_LEN)
|
||||
throw new IllegalArgumentException("hash32 != 32");
|
||||
if (prevHash32.length != 32) throw new IllegalArgumentException("prevHash32 != 32");
|
||||
if (signature64.length != SIGNATURE_LEN) throw new IllegalArgumentException("signature64 != 64");
|
||||
|
||||
this.recordNumber = recordNumber;
|
||||
this.prevHash32 = Arrays.copyOf(prevHash32, 32);
|
||||
this.blockNumber = blockNumber;
|
||||
this.timestamp = timestamp;
|
||||
this.lineIndex = lineIndex;
|
||||
this.lineNumber = lineNumber;
|
||||
this.type = type;
|
||||
this.subType = subType;
|
||||
this.version = version;
|
||||
this.bodyBytes = Arrays.copyOf(bodyBytes, bodyBytes.length);
|
||||
|
||||
// ✅ И при сборке — тоже сразу парсим body (чтобы объект был цельным)
|
||||
this.body = BodyRecordParser.parse(this.bodyBytes);
|
||||
|
||||
// ✅ УРОВЕНЬ B: проверка ожидаемой линии по типу body
|
||||
short expectedLine = this.body.expectedLineIndex();
|
||||
if (this.lineIndex != expectedLine) {
|
||||
throw new IllegalArgumentException(
|
||||
"Body is in wrong lineIndex: expected=" + expectedLine + " actual=" + this.lineIndex +
|
||||
" (type=" + this.body.type() + " ver=" + this.body.version() + ")"
|
||||
);
|
||||
}
|
||||
|
||||
this.signature64 = Arrays.copyOf(signature64, SIGNATURE_LEN);
|
||||
this.hash32 = Arrays.copyOf(hash32, HASH_LEN);
|
||||
|
||||
// recordSize теперь только RAW (header + body), без signature+hash
|
||||
this.recordSize = RAW_HEADER_SIZE + bodyBytes.length;
|
||||
this.blockSize = RAW_HEADER_SIZE + this.bodyBytes.length;
|
||||
|
||||
int fullLen = this.recordSize + SIGNATURE_LEN + HASH_LEN;
|
||||
// parse body по header
|
||||
this.body = BodyRecordParser.parse(this.type, this.subType, this.version, this.bodyBytes);
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(fullLen).order(ByteOrder.BIG_ENDIAN);
|
||||
bb.putInt(this.recordSize);
|
||||
bb.putInt(recordNumber);
|
||||
bb.putLong(timestamp);
|
||||
bb.putShort(lineIndex);
|
||||
bb.putInt(lineNumber);
|
||||
bb.put(bodyBytes);
|
||||
bb.put(this.signature64);
|
||||
bb.put(this.hash32);
|
||||
// build preimage
|
||||
ByteBuffer pre = ByteBuffer.allocate(blockSize).order(ByteOrder.BIG_ENDIAN);
|
||||
pre.put(this.prevHash32);
|
||||
pre.putInt(this.blockSize);
|
||||
pre.putInt(this.blockNumber);
|
||||
pre.putLong(this.timestamp);
|
||||
pre.putShort(this.type);
|
||||
pre.putShort(this.subType);
|
||||
pre.putShort(this.version);
|
||||
pre.put(this.bodyBytes);
|
||||
|
||||
this.fullBytes = bb.array();
|
||||
this.preimage = pre.array();
|
||||
this.hash32 = BchCryptoVerifier.sha256(preimage);
|
||||
|
||||
ByteBuffer full = ByteBuffer.allocate(blockSize + SIGNATURE_LEN).order(ByteOrder.BIG_ENDIAN);
|
||||
full.put(this.preimage);
|
||||
full.put(this.signature64);
|
||||
this.fullBytes = full.array();
|
||||
}
|
||||
|
||||
public byte[] getRawBytes() {
|
||||
int rawLen = recordSize; // ровно RAW, без signature+hash
|
||||
byte[] raw = new byte[rawLen];
|
||||
System.arraycopy(fullBytes, 0, raw, 0, rawLen);
|
||||
return raw;
|
||||
public byte[] getPreimageBytes() {
|
||||
return Arrays.copyOf(preimage, preimage.length);
|
||||
}
|
||||
|
||||
public byte[] getSignature64() {
|
||||
@@ -241,26 +210,18 @@ public final class BchBlockEntry {
|
||||
}
|
||||
|
||||
return "BchBlockEntry{"
|
||||
+ "RAW{"
|
||||
+ "recordSize=" + recordSize
|
||||
+ ", recordNumber=" + recordNumber
|
||||
+ "HDR{"
|
||||
+ "blockSize=" + blockSize
|
||||
+ ", blockNumber=" + blockNumber
|
||||
+ ", timestamp=" + timestamp + " (" + timeIso + ")"
|
||||
+ ", lineIndex=" + lineIndex
|
||||
+ ", lineNumber=" + lineNumber
|
||||
+ ", bodyLen=" + (bodyBytes == null ? -1 : bodyBytes.length)
|
||||
+ ", bodyType=" + (body == null ? "?" : (body.type() & 0xFFFF))
|
||||
+ ", bodyVer=" + (body == null ? "?" : (body.version() & 0xFFFF))
|
||||
+ ", type=" + (type & 0xFFFF)
|
||||
+ ", subType=" + (subType & 0xFFFF)
|
||||
+ ", version=" + (version & 0xFFFF)
|
||||
+ ", prevHash32(hex)=" + toHex(prevHash32)
|
||||
+ "}"
|
||||
+ ", TAIL{"
|
||||
+ "signature64(hex)=" + toHex(signature64)
|
||||
+ ", hash32(hex)=" + toHex(hash32)
|
||||
+ "}"
|
||||
+ ", FULL{"
|
||||
+ "fullLen=" + (fullBytes == null ? -1 : fullBytes.length)
|
||||
+ ", rawLen=" + recordSize
|
||||
+ "}"
|
||||
+ ", body=" + (body == null ? "null" : body.toString())
|
||||
+ ", bodyBytesPreview(hex32)=" + toHexPreview(bodyBytes, 32)
|
||||
+ ", BODY{len=" + (bodyBytes == null ? -1 : bodyBytes.length) + "}"
|
||||
+ ", TAIL{signature64(hex)=" + toHex(signature64) + "}"
|
||||
+ ", DERIVED{hash32(hex)=" + toHex(hash32) + "}"
|
||||
+ "}";
|
||||
}
|
||||
|
||||
@@ -275,14 +236,4 @@ public final class BchBlockEntry {
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
private static String toHexPreview(byte[] bytes, int maxBytes) {
|
||||
if (bytes == null) return "null";
|
||||
if (maxBytes <= 0) return "";
|
||||
int n = Math.min(bytes.length, maxBytes);
|
||||
byte[] cut = Arrays.copyOf(bytes, n);
|
||||
String hex = toHex(cut);
|
||||
if (bytes.length > n) hex += "…(+" + (bytes.length - n) + " байт)";
|
||||
return hex;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +1,26 @@
|
||||
// =======================
|
||||
// blockchain/BchCryptoVerifier.java (НОВАЯ ВЕРСИЯ под ТЗ)
|
||||
// =======================
|
||||
package blockchain;
|
||||
|
||||
import utils.config.ShineSignatureConstants;
|
||||
import utils.crypto.Ed25519Util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Новый верификатор по ТЗ:
|
||||
*
|
||||
* preimage = все байты блока без signature64
|
||||
* hash32 = SHA-256(preimage)
|
||||
* verify = Ed25519.verify(hash32, signature64, pubKey32)
|
||||
*/
|
||||
public final class BchCryptoVerifier {
|
||||
|
||||
private BchCryptoVerifier() {}
|
||||
|
||||
// ✅ строка — из констант; байты/длина — локально, “на месте”
|
||||
private static final String DOMAIN_STR = ShineSignatureConstants.BLOCK_HASH_DOMAIN;
|
||||
private static final byte[] DOMAIN = DOMAIN_STR.getBytes(StandardCharsets.US_ASCII);
|
||||
private static final int DOMAIN_LEN = DOMAIN.length;
|
||||
|
||||
/**
|
||||
* preimage =
|
||||
* DOMAIN +
|
||||
* [1] loginLen + loginBytes +
|
||||
* prevGlobalHash32 +
|
||||
* prevLineHash32 +
|
||||
* rawBytes
|
||||
*/
|
||||
public static byte[] buildPreimage(String userLogin,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
byte[] rawBytes) {
|
||||
|
||||
Objects.requireNonNull(userLogin, "userLogin == null");
|
||||
Objects.requireNonNull(prevGlobalHash32, "prevGlobalHash32 == null");
|
||||
Objects.requireNonNull(prevLineHash32, "prevLineHash32 == null");
|
||||
Objects.requireNonNull(rawBytes, "rawBytes == null");
|
||||
|
||||
if (prevGlobalHash32.length != 32 || prevLineHash32.length != 32)
|
||||
throw new IllegalArgumentException("hash len != 32");
|
||||
|
||||
byte[] loginBytes = userLogin.getBytes(StandardCharsets.UTF_8);
|
||||
if (loginBytes.length > 255)
|
||||
throw new IllegalArgumentException("login >255 bytes");
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(
|
||||
DOMAIN_LEN +
|
||||
1 + loginBytes.length +
|
||||
32 + 32 +
|
||||
rawBytes.length
|
||||
).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.put(DOMAIN);
|
||||
bb.put((byte) loginBytes.length);
|
||||
bb.put(loginBytes);
|
||||
bb.put(prevGlobalHash32);
|
||||
bb.put(prevLineHash32);
|
||||
bb.put(rawBytes);
|
||||
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
public static byte[] sha256(byte[] data) {
|
||||
Objects.requireNonNull(data, "data == null");
|
||||
try {
|
||||
MessageDigest d = MessageDigest.getInstance("SHA-256");
|
||||
return d.digest(data);
|
||||
@@ -69,34 +29,15 @@ public final class BchCryptoVerifier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка подписи Ed25519:
|
||||
*/
|
||||
public static boolean verifyAll(String userLogin,
|
||||
byte[] prevGlobalHash32,
|
||||
byte[] prevLineHash32,
|
||||
byte[] rawBytes,
|
||||
byte[] signature64,
|
||||
byte[] publicKey32,
|
||||
byte[] expectedHash32FromBlock) {
|
||||
|
||||
Objects.requireNonNull(signature64, "signature64 == null");
|
||||
public static boolean verifyBlock(BchBlockEntry block, byte[] publicKey32) {
|
||||
Objects.requireNonNull(block, "block == null");
|
||||
Objects.requireNonNull(publicKey32, "publicKey32 == null");
|
||||
Objects.requireNonNull(expectedHash32FromBlock, "expectedHash32FromBlock == null");
|
||||
|
||||
if (signature64.length != 64) throw new IllegalArgumentException("signature64 != 64");
|
||||
if (publicKey32.length != 32) throw new IllegalArgumentException("publicKey32 != 32");
|
||||
if (expectedHash32FromBlock.length != 32) throw new IllegalArgumentException("hash32 != 32");
|
||||
|
||||
byte[] preimage = buildPreimage(userLogin, prevGlobalHash32, prevLineHash32, rawBytes);
|
||||
byte[] hash32 = sha256(preimage);
|
||||
byte[] hash32 = block.getHash32();
|
||||
byte[] sig64 = block.getSignature64();
|
||||
|
||||
// 1) сверяем hash, который лежит в блоке
|
||||
if (!java.util.Arrays.equals(hash32, expectedHash32FromBlock)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2) проверяем подпись (Ed25519 над hash32)
|
||||
return Ed25519Util.verify(hash32, signature64, publicKey32);
|
||||
return Ed25519Util.verify(hash32, sig64, publicKey32);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
// =======================
|
||||
// blockchain/body/BodyHasTarget.java (без изменений, оставляю как есть)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
/**
|
||||
* BodyToFields — дополнительный интерфейс для body, которые "ссылаются" на цель (to-поля).
|
||||
* BodyHasTarget — дополнительный интерфейс для body, которые "ссылаются" на цель (to-поля).
|
||||
*
|
||||
* Идея:
|
||||
* - Не все body имеют "to".
|
||||
@@ -10,11 +13,6 @@ package blockchain.body;
|
||||
*
|
||||
* Важно:
|
||||
* - Все методы могут возвращать null.
|
||||
* - toLogin может отсутствовать в самом формате body (например, ReactionBody, TextBody reply/repost),
|
||||
* но в БД мы пишем toLogin "про запас".
|
||||
* Поэтому writer может:
|
||||
* - взять toLogin из body (если есть),
|
||||
* - либо попытаться вычислить из toBchName.
|
||||
*/
|
||||
public interface BodyHasTarget {
|
||||
|
||||
|
||||
@@ -1,56 +1,29 @@
|
||||
// =======================
|
||||
// blockchain/body/BodyRecord.java (ИЗМЕНЁННЫЙ контракт под ТЗ)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
/**
|
||||
* BodyRecord_new — общий контракт для всех типов body (тела блока).
|
||||
* BodyRecord — общий контракт для всех типов body (тела блока).
|
||||
*
|
||||
* Идея:
|
||||
* - На каждый тип body (Header, Text, Reaction, ...) — отдельный класс.
|
||||
* - Десериализация из байтов делается КОНСТРУКТОРОМ:
|
||||
* new XxxBody_new(byte[] bodyBytes)
|
||||
* (конструктор обязан распарсить байты или кинуть IllegalArgumentException).
|
||||
* ВАЖНО (новый формат):
|
||||
* - type/subType/version НЕ лежат в bodyBytes.
|
||||
* - type/subType/version читаются из заголовка блока (BchBlockEntry).
|
||||
*
|
||||
* - Валидация делается методом check().
|
||||
* check() должен:
|
||||
* - вернуть this, если всё корректно
|
||||
* - кинуть IllegalArgumentException, если данные некорректны
|
||||
*
|
||||
* - Сериализация обратно в байты делается методом toBytes().
|
||||
*
|
||||
* - type() и version() — это идентификаторы формата body.
|
||||
* Они должны быть константами для класса (например TYPE=1, VERSION=1).
|
||||
*
|
||||
* ДОПОЛНЕНИЕ (ЛИНИИ):
|
||||
* - Каждый тип body знает, в какой lineIndex он ДОЛЖЕН находиться.
|
||||
* Это проверяется в валидаторе блока (уровень B).
|
||||
*
|
||||
* ДОПОЛНЕНИЕ (SUBTYPE):
|
||||
* - У каждого body есть subType (uint16).
|
||||
* - Для HeaderBody он всегда 0 (служебная совместимость).
|
||||
* - Для TextBody это тип сообщения (NEW/REPLY/REPOST).
|
||||
* - Для ReactionBody это тип реакции (LIKE и т.п.).
|
||||
* Поэтому из интерфейса УБРАНЫ:
|
||||
* - type()
|
||||
* - subType()
|
||||
* - version()
|
||||
* - expectedLineIndex()
|
||||
*/
|
||||
public interface BodyRecord {
|
||||
|
||||
/** Код типа записи (совпадает с type в bodyBytes). */
|
||||
short type();
|
||||
|
||||
/** Версия формата записи (совпадает с version в bodyBytes). */
|
||||
short version();
|
||||
|
||||
/**
|
||||
* Подтип записи (uint16).
|
||||
*/
|
||||
short subType();
|
||||
|
||||
/** Ожидаемый индекс линии для этого body. */
|
||||
short expectedLineIndex();
|
||||
|
||||
/** Проверить корректность содержимого и вернуть этот объект (или кинуть исключение). */
|
||||
BodyRecord check();
|
||||
|
||||
/**
|
||||
* Сериализовать тело записи в байты (ровно то, что кладётся в block.body).
|
||||
* Важно: включает type/version/subType и весь payload.
|
||||
* Сериализовать тело записи в байты (ровно то, что кладётся в block.bodyBytes).
|
||||
* Важно: НЕ включает type/subType/version.
|
||||
*/
|
||||
byte[] toBytes();
|
||||
}
|
||||
@@ -1,31 +1,34 @@
|
||||
// =======================
|
||||
// blockchain/body/BodyRecordParser.java (ИЗМЕНЁННЫЙ под новый формат)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Парсер body теперь выбирает класс по header: type/subType/version,
|
||||
* потому что bodyBytes больше НЕ содержат type/subType/version.
|
||||
*/
|
||||
public final class BodyRecordParser {
|
||||
|
||||
private BodyRecordParser() {}
|
||||
|
||||
public static BodyRecord parse(byte[] bodyBytes) {
|
||||
public static BodyRecord parse(short type, short subType, short version, byte[] bodyBytes) {
|
||||
if (bodyBytes == null) throw new IllegalArgumentException("bodyBytes == null");
|
||||
if (bodyBytes.length < 4) throw new IllegalArgumentException("bodyBytes too short (<4)");
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
short type = bb.getShort();
|
||||
short ver = bb.getShort();
|
||||
int t = type & 0xFFFF;
|
||||
int v = version & 0xFFFF;
|
||||
|
||||
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
|
||||
// ключ = (type<<16)|version (как раньше по смыслу), но берём из HEADER
|
||||
int key = (t << 16) | v;
|
||||
|
||||
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 UserParamBody.KEY -> new UserParamBody(bodyBytes); // type=4, ver=1 параметры пользователя
|
||||
case HeaderBody.KEY -> new HeaderBody(subType, version, bodyBytes);
|
||||
case TextBody.KEY -> new TextBody(subType, version, bodyBytes);
|
||||
case ReactionBody.KEY -> new ReactionBody(subType, version, bodyBytes);
|
||||
case ConnectionBody.KEY -> new ConnectionBody(subType, version, bodyBytes);
|
||||
case UserParamBody.KEY -> new UserParamBody(subType, version, bodyBytes);
|
||||
default -> throw new IllegalArgumentException(String.format(
|
||||
"Unknown body type/version: type=%d ver=%d (key=0x%08X)",
|
||||
(type & 0xFFFF), (ver & 0xFFFF), key
|
||||
"Unknown body type/version from header: type=%d ver=%d subType=%d",
|
||||
t, v, (subType & 0xFFFF)
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// =======================
|
||||
// blockchain/body/ConnectionBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, + line fields)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
import blockchain.LineIndex;
|
||||
import shine.db.MsgSubType;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
@@ -9,112 +12,75 @@ import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ConnectionBody — type=3, ver=1. (Связь/отношение)
|
||||
* ConnectionBody — type=3, ver=1 (в заголовке блока).
|
||||
*
|
||||
* Идея:
|
||||
* - Это запись "у меня есть связь с X" ИЛИ "я отменяю связь с X".
|
||||
* - subType определяет вид связи и действие.
|
||||
* subType (в заголовке блока) как MsgSubType:
|
||||
* FRIEND=10, UNFRIEND=11
|
||||
* CONTACT=20, UNCONTACT=21
|
||||
* FOLLOW=30, UNFOLLOW=31
|
||||
*
|
||||
* subType (uint16):
|
||||
* УСТАНОВИТЬ связь:
|
||||
* 10 = FRIEND (друг)
|
||||
* 20 = CONTACT (контакт)
|
||||
* 30 = FOLLOW (подписан на кого-то)
|
||||
*
|
||||
* ОТМЕНИТЬ связь (событие, которое “снимает” прошлую связь):
|
||||
* 11 = UNFRIEND (больше не друг)
|
||||
* 21 = UNCONTACT (больше не контакт)
|
||||
* 31 = UNFOLLOW (больше не подписан)
|
||||
*
|
||||
* Важно про смысл:
|
||||
* - Состояние связи вычисляется по последнему блоку данной “категории”:
|
||||
* (toLogin, kind=FRIEND/CONTACT/FOLLOW)
|
||||
* Если последний subType — 10/20/30 => связь активна
|
||||
* Если последний subType — 11/21/31 => связь снята
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [2] type=3
|
||||
* [2] ver=1
|
||||
*
|
||||
* [2] subType (uint16) — вид связи (10,20/30)
|
||||
* bodyBytes (BigEndian), новый формат:
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber
|
||||
*
|
||||
* [1] toLoginLen (uint8)
|
||||
* [N] toLogin UTF-8
|
||||
* ВАЖНО: toLogin — это "с кем связь" (ключевой смысл этой записи).
|
||||
*
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [M] toBlockchainName UTF-8
|
||||
* [4] toBlockGlobalNumber (int32)
|
||||
* [32] toBlockHash32 (raw 32 bytes)
|
||||
*
|
||||
* ВАЖНО: поля toBlockchainName/toBlockGlobalNumber/toBlockHash32 — это
|
||||
* "последний известный блок" того человека (снимок/якорь состояния).
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=3 (выделяем отдельную линию под связи).
|
||||
*/
|
||||
public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
public final class ConnectionBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
|
||||
public static final short TYPE = 3;
|
||||
public static final short VER = 1;
|
||||
|
||||
/** Удобный ключ для BodyRecordParser: (type<<16)|ver */
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
// --- subType: SET ---
|
||||
public static final short SUB_FRIEND = 10;
|
||||
public static final short SUB_CONTACT = 20;
|
||||
public static final short SUB_FOLLOW = 30;
|
||||
public final short subType; // из header
|
||||
public final short version; // из header
|
||||
|
||||
// --- subType: UNSET (снятие/отмена связи) ---
|
||||
public static final short SUB_UNFRIEND = 11; // больше не друг
|
||||
public static final short SUB_UNCONTACT = 21; // больше не контакт
|
||||
public static final short SUB_UNFOLLOW = 31; // больше не подписан
|
||||
// line
|
||||
public final int prevLineNumber;
|
||||
public final byte[] prevLineHash32;
|
||||
public final int thisLineNumber;
|
||||
|
||||
public final short subType;
|
||||
|
||||
/** С кем связь (главное поле). */
|
||||
// payload
|
||||
public final String toLogin;
|
||||
|
||||
/** Блокчейн того человека (снимок/якорь). */
|
||||
public final String toBlockchainName;
|
||||
|
||||
/** Номер последнего известного блока у того человека (снимок/якорь). */
|
||||
public final int toBlockGlobalNumber;
|
||||
|
||||
/** Хэш последнего известного блока у того человека (снимок/якорь). */
|
||||
public final byte[] toBlockHash32;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор из байт =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
public ConnectionBody(byte[] bodyBytes) {
|
||||
public ConnectionBody(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("ConnectionBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||
}
|
||||
if (!isValidSubType(this.subType)) {
|
||||
throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
|
||||
// минимум:
|
||||
// type[2]+ver[2]+subType[2] +
|
||||
// toLoginLen[1]+toLogin[1] +
|
||||
// toBchLen[1]+toBch[1] +
|
||||
// global[4] + hash[32]
|
||||
if (bodyBytes.length < 2 + 2 + 2 + 1 + 1 + 1 + 1 + 4 + 32) {
|
||||
// line(4+32+4) + toLoginLen[1]+toLogin[1] + toBchLen[1]+toBch[1] + global[4] + hash[32]
|
||||
if (bodyBytes.length < (4 + 32 + 4) + 1 + 1 + 1 + 1 + 4 + 32) {
|
||||
throw new IllegalArgumentException("ConnectionBody 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 ConnectionBody: type=" + type + " ver=" + ver);
|
||||
}
|
||||
this.prevLineNumber = bb.getInt();
|
||||
|
||||
this.subType = bb.getShort();
|
||||
if (!isValidSubType(this.subType)) {
|
||||
throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
this.prevLineHash32 = new byte[32];
|
||||
bb.get(this.prevLineHash32);
|
||||
|
||||
this.thisLineNumber = bb.getInt();
|
||||
|
||||
// --- toLogin ---
|
||||
int toLoginLen = Byte.toUnsignedInt(bb.get());
|
||||
if (toLoginLen <= 0) throw new IllegalArgumentException("toLoginLen is 0");
|
||||
if (bb.remaining() < toLoginLen) throw new IllegalArgumentException("toLogin payload too short");
|
||||
@@ -123,8 +89,6 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
bb.get(toLoginBytes);
|
||||
this.toLogin = new String(toLoginBytes, StandardCharsets.UTF_8);
|
||||
|
||||
// --- toBlockchainName + snapshot блока ---
|
||||
if (bb.remaining() < 1) throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
||||
int bchLen = Byte.toUnsignedInt(bb.get());
|
||||
if (bchLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||
if (bb.remaining() < bchLen + 4 + 32) throw new IllegalArgumentException("Connection payload too short");
|
||||
@@ -138,17 +102,13 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
this.toBlockHash32 = new byte[32];
|
||||
bb.get(this.toBlockHash32);
|
||||
|
||||
// запрет мусора в конце
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор “вручную” ========================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
public ConnectionBody(short subType,
|
||||
public ConnectionBody(int prevLineNumber,
|
||||
byte[] prevLineHash32,
|
||||
int thisLineNumber,
|
||||
short subType,
|
||||
String toLogin,
|
||||
String toBlockchainName,
|
||||
int toBlockGlobalNumber,
|
||||
@@ -158,19 +118,21 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
|
||||
if (!isValidSubType(subType)) {
|
||||
throw new IllegalArgumentException("Unknown connection subType: " + (subType & 0xFFFF));
|
||||
}
|
||||
|
||||
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
||||
if (toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
|
||||
if (!toLogin.matches("^[A-Za-z0-9_]+$"))
|
||||
throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
||||
if (!toLogin.matches("^[A-Za-z0-9_]+$")) throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
||||
|
||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.prevLineNumber = prevLineNumber;
|
||||
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
this.thisLineNumber = thisLineNumber;
|
||||
|
||||
this.subType = subType;
|
||||
this.version = VER;
|
||||
|
||||
this.toLogin = toLogin;
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
@@ -178,62 +140,33 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
}
|
||||
|
||||
private static boolean isValidSubType(short st) {
|
||||
return st == SUB_FRIEND || st == SUB_CONTACT || st == SUB_FOLLOW
|
||||
|| st == SUB_UNFRIEND || st == SUB_UNCONTACT || st == SUB_UNFOLLOW;
|
||||
}
|
||||
|
||||
/** true если это событие установки связи (10/20/30). */
|
||||
public boolean isSetAction() {
|
||||
return subType == SUB_FRIEND || subType == SUB_CONTACT || subType == SUB_FOLLOW;
|
||||
}
|
||||
|
||||
/** true если это событие снятия связи (11/21/31). */
|
||||
public boolean isUnsetAction() {
|
||||
return subType == SUB_UNFRIEND || subType == SUB_UNCONTACT || subType == SUB_UNFOLLOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Нормализованный “вид связи” без действия:
|
||||
* FRIEND / CONTACT / FOLLOW
|
||||
*/
|
||||
public short kind() {
|
||||
return switch (subType) {
|
||||
case SUB_FRIEND, SUB_UNFRIEND -> SUB_FRIEND;
|
||||
case SUB_CONTACT, SUB_UNCONTACT -> SUB_CONTACT;
|
||||
case SUB_FOLLOW, SUB_UNFOLLOW -> SUB_FOLLOW;
|
||||
default -> throw new IllegalStateException("Unexpected subType: " + (subType & 0xFFFF));
|
||||
};
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyRecord контракт =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
@Override public short type() { return TYPE; }
|
||||
@Override public short version() { return VER; }
|
||||
@Override public short subType() { return subType; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return LineIndex.CONNECTION;
|
||||
int v = st & 0xFFFF;
|
||||
return v == (MsgSubType.CONNECTION_FRIEND & 0xFFFF)
|
||||
|| v == (MsgSubType.CONNECTION_UNFRIEND & 0xFFFF)
|
||||
|| v == (MsgSubType.CONNECTION_CONTACT & 0xFFFF)
|
||||
|| v == (MsgSubType.CONNECTION_UNCONTACT & 0xFFFF)
|
||||
|| v == (MsgSubType.CONNECTION_FOLLOW & 0xFFFF)
|
||||
|| v == (MsgSubType.CONNECTION_UNFOLLOW & 0xFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionBody check() {
|
||||
if (!isValidSubType(subType))
|
||||
throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
||||
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
||||
|
||||
if (toLogin == null || toLogin.isBlank())
|
||||
throw new IllegalArgumentException("toLogin is blank");
|
||||
if (!toLogin.matches("^[A-Za-z0-9_]+$"))
|
||||
throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
||||
// line rule
|
||||
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 (toBlockchainName == null || toBlockchainName.isBlank())
|
||||
throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0)
|
||||
throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
if (toLogin == null || toLogin.isBlank()) throw new IllegalArgumentException("toLogin is blank");
|
||||
if (!toLogin.matches("^[A-Za-z0-9_]+$")) throw new IllegalArgumentException("toLogin must match ^[A-Za-z0-9_]+$");
|
||||
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -248,26 +181,19 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
if (bchBytes.length == 0 || bchBytes.length > 255)
|
||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||
|
||||
if (!isValidSubType(subType))
|
||||
throw new IllegalArgumentException("Bad connection subType: " + (subType & 0xFFFF));
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
// type[2]+ver[2]+subType[2]
|
||||
// + toLoginLen[1]+toLogin[N]
|
||||
// + toBchLen[1]+toBch[M]
|
||||
// + global[4]+hash[32]
|
||||
int cap = 2 + 2 + 2
|
||||
int cap = (4 + 32 + 4)
|
||||
+ 1 + toLoginBytes.length
|
||||
+ 1 + bchBytes.length
|
||||
+ 4 + 32;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.putShort(TYPE);
|
||||
bb.putShort(VER);
|
||||
|
||||
bb.putShort(subType);
|
||||
bb.putInt(prevLineNumber);
|
||||
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
bb.putInt(thisLineNumber);
|
||||
|
||||
bb.put((byte) toLoginBytes.length);
|
||||
bb.put(toLoginBytes);
|
||||
@@ -281,69 +207,20 @@ public final class ConnectionBody implements BodyRecord, BodyHasTarget {
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String st = switch (subType) {
|
||||
case SUB_FRIEND -> "FRIEND (10)";
|
||||
case SUB_CONTACT -> "CONTACT (20)";
|
||||
case SUB_FOLLOW -> "FOLLOW (30)";
|
||||
case SUB_UNFRIEND -> "UNFRIEND (11)";
|
||||
case SUB_UNCONTACT -> "UNCONTACT (21)";
|
||||
case SUB_UNFOLLOW -> "UNFOLLOW (31)";
|
||||
default -> "UNKNOWN";
|
||||
};
|
||||
|
||||
String action = isSetAction() ? "SET" : (isUnsetAction() ? "UNSET" : "?");
|
||||
String kindStr = switch (kind()) {
|
||||
case SUB_FRIEND -> "FRIEND";
|
||||
case SUB_CONTACT -> "CONTACT";
|
||||
case SUB_FOLLOW -> "FOLLOW";
|
||||
default -> "?";
|
||||
};
|
||||
|
||||
return """
|
||||
ConnectionBody {
|
||||
тип записи : CONNECTION (type=3, ver=1)
|
||||
ожидаемая линия : 3
|
||||
subType : %s
|
||||
действие : %s
|
||||
вид связи : %s
|
||||
связь с login : "%s"
|
||||
блокчейн друга/цели : "%s"
|
||||
lastKnown globalNumber : %d
|
||||
lastKnown hash (hex) : %s
|
||||
}
|
||||
""".formatted(
|
||||
st,
|
||||
action,
|
||||
kindStr,
|
||||
toLogin,
|
||||
toBlockchainName,
|
||||
toBlockGlobalNumber,
|
||||
toBlockHashHex()
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
public String toBlockHashHex() {
|
||||
char[] HEX = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[64];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int v = toBlockHash32[i] & 0xFF;
|
||||
out[i * 2] = HEX[v >>> 4];
|
||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyHasTarget контракт ========================= */
|
||||
/* ===================================================================== */
|
||||
/* ====================== 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; }
|
||||
|
||||
/* ====================== BodyHasTarget ===================== */
|
||||
@Override public String toLogin() { return toLogin; }
|
||||
|
||||
@Override public String toBchName() { return toBlockchainName; }
|
||||
|
||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
|
||||
@Override public byte[] toBlockHasheBytes() { return toBlockHash32; }
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// =======================
|
||||
// blockchain/body/HeaderBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
import blockchain.LineIndex;
|
||||
import utils.config.ShineSignatureConstants;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -11,18 +13,13 @@ import java.util.Objects;
|
||||
/**
|
||||
* HeaderBody — type=0, version=1.
|
||||
*
|
||||
* Полный bodyBytes (BigEndian):
|
||||
* [2] type=0
|
||||
* [2] version=1
|
||||
*
|
||||
* [2] subType (uint16) = 0
|
||||
* В новом формате type/subType/version живут в HEADER блока,
|
||||
* поэтому bodyBytes для HeaderBody содержат только payload:
|
||||
*
|
||||
* bodyBytes (BigEndian):
|
||||
* [TAG_LEN] tag ASCII "SHiNE"
|
||||
* [1] loginLength=N (uint8)
|
||||
* [N] login UTF-8
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=0 (genesis)
|
||||
*/
|
||||
public final class HeaderBody implements BodyRecord {
|
||||
|
||||
@@ -31,40 +28,39 @@ public final class HeaderBody implements BodyRecord {
|
||||
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
/** Для header всегда 0 (служебная совместимость). */
|
||||
/** Для header subType всегда 0 (служебная совместимость). */
|
||||
public static final short SUBTYPE_COMPAT = 0;
|
||||
|
||||
/** TAG формата (ASCII). Значение берём из общих строковых констант. */
|
||||
/** TAG формата (ASCII). */
|
||||
public static final String TAG = ShineSignatureConstants.BLOCKCHAIN_HEADER_TAG;
|
||||
|
||||
// ✅ производные значения считаем "на месте", а не в константах
|
||||
private static final byte[] TAG_ASCII = TAG.getBytes(StandardCharsets.US_ASCII);
|
||||
private static final int TAG_LEN = TAG_ASCII.length;
|
||||
|
||||
public final short subType; // всегда 0
|
||||
public final short subType; // всегда 0 (из заголовка блока)
|
||||
public final short version; // из заголовка блока
|
||||
public final String tag; // "SHiNE"
|
||||
public final String login;
|
||||
|
||||
/** Десериализация из полного bodyBytes (включая type/version/subType). */
|
||||
public HeaderBody(byte[] bodyBytes) {
|
||||
/** Десериализация из payload bodyBytes (без type/subType/version). */
|
||||
public HeaderBody(short subType, short version, byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
if (bodyBytes.length < 4 + 2) throw new IllegalArgumentException("HeaderBody too short (<6)");
|
||||
|
||||
this.subType = subType;
|
||||
this.version = version;
|
||||
|
||||
if ((this.subType & 0xFFFF) != (SUBTYPE_COMPAT & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("HeaderBody subType must be 0, got=" + (this.subType & 0xFFFF));
|
||||
}
|
||||
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("HeaderBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||
}
|
||||
|
||||
// минимум: tag[TAG_LEN] + loginLen[1]
|
||||
if (bodyBytes.length < TAG_LEN + 1) throw new IllegalArgumentException("HeaderBody 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 HeaderBody: type=" + type + " ver=" + ver);
|
||||
|
||||
this.subType = bb.getShort();
|
||||
if (this.subType != SUBTYPE_COMPAT)
|
||||
throw new IllegalArgumentException("HeaderBody subType must be 0, got=" + (this.subType & 0xFFFF));
|
||||
|
||||
// дальше: tag[TAG_LEN] + loginLen[1] минимум
|
||||
if (bb.remaining() < TAG_LEN + 1)
|
||||
throw new IllegalArgumentException("Header payload too short");
|
||||
|
||||
byte[] tagBytes = new byte[TAG_LEN];
|
||||
bb.get(tagBytes);
|
||||
String t = new String(tagBytes, StandardCharsets.US_ASCII);
|
||||
@@ -79,59 +75,43 @@ public final class HeaderBody implements BodyRecord {
|
||||
bb.get(loginBytes);
|
||||
this.login = new String(loginBytes, StandardCharsets.UTF_8);
|
||||
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
|
||||
/** Создание “вручную” (для генерации первого блока). */
|
||||
/** Создание “вручную”. */
|
||||
public HeaderBody(String login) {
|
||||
Objects.requireNonNull(login, "login == null");
|
||||
this.subType = SUBTYPE_COMPAT;
|
||||
this.version = VER;
|
||||
this.tag = TAG;
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
@Override public short type() { return TYPE; }
|
||||
@Override public short version() { return VER; }
|
||||
@Override public short subType() { return subType; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return LineIndex.HEADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderBody check() {
|
||||
if (subType != SUBTYPE_COMPAT)
|
||||
if ((subType & 0xFFFF) != (SUBTYPE_COMPAT & 0xFFFF))
|
||||
throw new IllegalArgumentException("HeaderBody subType must be 0");
|
||||
|
||||
if (login == null || login.isBlank())
|
||||
throw new IllegalArgumentException("Login is blank");
|
||||
if (!login.matches("^[A-Za-z0-9_]+$"))
|
||||
throw new IllegalArgumentException("Login must match ^[A-Za-z0-9_]+$");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBytes() {
|
||||
byte[] loginUtf8 = login.getBytes(StandardCharsets.UTF_8);
|
||||
if (loginUtf8.length > 255)
|
||||
throw new IllegalArgumentException("Login too long (>255 bytes)");
|
||||
if (loginUtf8.length == 0 || loginUtf8.length > 255)
|
||||
throw new IllegalArgumentException("Login utf8 len must be 1..255");
|
||||
|
||||
// type[2] + ver[2] + subType[2] + tag[TAG_LEN] + loginLen[1] + login[N]
|
||||
int cap = 2 + 2 + 2 + TAG_LEN + 1 + loginUtf8.length;
|
||||
int cap = TAG_LEN + 1 + loginUtf8.length;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.putShort(TYPE);
|
||||
bb.putShort(VER);
|
||||
|
||||
bb.putShort(SUBTYPE_COMPAT);
|
||||
|
||||
bb.put(TAG_ASCII); // [TAG_LEN]
|
||||
bb.put((byte) loginUtf8.length); // [1]
|
||||
bb.put(loginUtf8); // [N]
|
||||
bb.put(TAG_ASCII);
|
||||
bb.put((byte) loginUtf8.length);
|
||||
bb.put(loginUtf8);
|
||||
|
||||
return bb.array();
|
||||
}
|
||||
@@ -140,8 +120,7 @@ public final class HeaderBody implements BodyRecord {
|
||||
public String toString() {
|
||||
return """
|
||||
HeaderBody {
|
||||
тип записи : HEADER (type=0, ver=1)
|
||||
ожидаемая линия : 0 (genesis)
|
||||
тип записи : HEADER (type=0, ver=1) [в заголовке блока]
|
||||
subType : 0 (compat)
|
||||
тег формата : "%s"
|
||||
login владельца : "%s"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// =======================
|
||||
// blockchain/body/ReactionBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, НЕТ линейных полей)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
import blockchain.LineIndex;
|
||||
import shine.db.MsgSubType;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
@@ -9,22 +12,18 @@ import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* ReactionBody — type=2, version=1.
|
||||
* ReactionBody — type=2, version=1 (в заголовке блока).
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [2] type=2
|
||||
* [2] ver=1
|
||||
*
|
||||
* [2] subType (uint16) — подтип реакции
|
||||
* 1 = LIKE (лайк)
|
||||
* subType (в заголовке блока):
|
||||
* 1 = LIKE
|
||||
*
|
||||
* bodyBytes (BigEndian), новый формат:
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [N] toBlockchainName UTF-8
|
||||
* [4] toBlockGlobalNumber (int32)
|
||||
* [32] toBlockHash32 (raw 32 bytes)
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=2
|
||||
* ЛИНИИ НЕТ.
|
||||
*/
|
||||
public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
@@ -33,40 +32,34 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
// subType:
|
||||
public static final short SUB_LIKE = 1;
|
||||
|
||||
public final short subType;
|
||||
public final short subType; // из header
|
||||
public final short version; // из header
|
||||
|
||||
public final String toBlockchainName;
|
||||
public final int toBlockGlobalNumber;
|
||||
public final byte[] toBlockHash32;
|
||||
|
||||
/** Десериализация из полного bodyBytes (включая type/version/subType). */
|
||||
public ReactionBody(byte[] bodyBytes) {
|
||||
public ReactionBody(short subType, short version, byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
|
||||
// минимум: type[2]+ver[2]+subType[2]+nameLen[1]+name[1]+global[4]+hash[32]
|
||||
if (bodyBytes.length < 2 + 2 + 2 + 1 + 1 + 4 + 32) {
|
||||
throw new IllegalArgumentException("ReactionBody too short");
|
||||
this.subType = subType;
|
||||
this.version = version;
|
||||
|
||||
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("ReactionBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||
}
|
||||
|
||||
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 ReactionBody: type=" + type + " ver=" + ver);
|
||||
|
||||
this.subType = bb.getShort();
|
||||
if (this.subType != SUB_LIKE) {
|
||||
if ((this.subType & 0xFFFF) != (MsgSubType.REACTION_LIKE & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("Bad reaction subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
|
||||
// минимум: nameLen[1]+name[1]+global[4]+hash[32]
|
||||
if (bodyBytes.length < 1 + 1 + 4 + 32) throw new IllegalArgumentException("ReactionBody too short");
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||
if (bb.remaining() < nameLen + 4 + 32)
|
||||
throw new IllegalArgumentException("ReactionBody payload too short");
|
||||
if (bb.remaining() < nameLen + 4 + 32) throw new IllegalArgumentException("ReactionBody payload too short");
|
||||
|
||||
byte[] nameBytes = new byte[nameLen];
|
||||
bb.get(nameBytes);
|
||||
@@ -77,46 +70,28 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
this.toBlockHash32 = new byte[32];
|
||||
bb.get(this.toBlockHash32);
|
||||
|
||||
// запрет мусора в конце
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
|
||||
/** Создание “вручную”. */
|
||||
public ReactionBody(short subType,
|
||||
String toBlockchainName,
|
||||
int toBlockGlobalNumber,
|
||||
byte[] toBlockHash32) {
|
||||
|
||||
public ReactionBody(String toBlockchainName, int toBlockGlobalNumber, byte[] toBlockHash32) {
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
|
||||
if (subType != SUB_LIKE)
|
||||
throw new IllegalArgumentException("Unknown reaction subType: " + (subType & 0xFFFF));
|
||||
this.subType = MsgSubType.REACTION_LIKE;
|
||||
this.version = VER;
|
||||
|
||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.subType = subType;
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
}
|
||||
|
||||
@Override public short type() { return TYPE; }
|
||||
@Override public short version() { return VER; }
|
||||
@Override public short subType() { return subType; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return LineIndex.REACTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReactionBody check() {
|
||||
if (subType != SUB_LIKE)
|
||||
if ((subType & 0xFFFF) != (MsgSubType.REACTION_LIKE & 0xFFFF))
|
||||
throw new IllegalArgumentException("Bad reaction subType: " + (subType & 0xFFFF));
|
||||
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank())
|
||||
@@ -135,16 +110,9 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
if (nameBytes.length == 0 || nameBytes.length > 255)
|
||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||
|
||||
// type[2]+ver[2]+subType[2] + nameLen[1]+name[N] + global[4] + hash[32]
|
||||
int cap = 2 + 2 + 2 + 1 + nameBytes.length + 4 + 32;
|
||||
int cap = 1 + nameBytes.length + 4 + 32;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.putShort(TYPE);
|
||||
bb.putShort(VER);
|
||||
|
||||
bb.putShort(subType);
|
||||
|
||||
bb.put((byte) nameBytes.length);
|
||||
bb.put(nameBytes);
|
||||
bb.putInt(toBlockGlobalNumber);
|
||||
@@ -153,43 +121,8 @@ public final class ReactionBody implements BodyRecord, BodyHasTarget {
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String st = (subType == SUB_LIKE) ? "LIKE (1)" : "UNKNOWN";
|
||||
/* ====================== BodyHasTarget ====================== */
|
||||
|
||||
return """
|
||||
ReactionBody {
|
||||
тип записи : REACTION (type=2, ver=1)
|
||||
ожидаемая линия : 2
|
||||
subType : %s
|
||||
целевой блокчейн : "%s"
|
||||
globalNumber цели : %d
|
||||
hash цели (hex) : %s
|
||||
}
|
||||
""".formatted(
|
||||
st,
|
||||
toBlockchainName,
|
||||
toBlockGlobalNumber,
|
||||
toBlockHashHex()
|
||||
);
|
||||
}
|
||||
|
||||
public String toBlockHashHex() {
|
||||
char[] HEX = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[64];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int v = toBlockHash32[i] & 0xFF;
|
||||
out[i * 2] = HEX[v >>> 4];
|
||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyHasTarget контракт ========================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
/** В самом формате ReactionBody login цели не хранится => null. */
|
||||
@Override public String toLogin() { return null; }
|
||||
|
||||
@Override public String toBchName() { return toBlockchainName; }
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// =======================
|
||||
// blockchain/body/TextBody.java (ИЗМЕНЁННЫЙ: header содержит type/subType/version, body содержит line fields)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
import blockchain.LineIndex;
|
||||
import shine.db.MsgSubType;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
@@ -11,98 +14,87 @@ import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* TextBody — type=1, ver=1.
|
||||
* TextBody — type=1, ver=1 (в заголовке блока).
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [2] type=1
|
||||
* [2] ver=1
|
||||
* subType (в заголовке блока):
|
||||
* 1 = NEW
|
||||
* 2 = REPLY
|
||||
* 3 = REPOST
|
||||
* 10 = EDIT
|
||||
*
|
||||
* [2] subType (uint16): подтип текстового сообщения
|
||||
* 1 = новое сообщение (начало ветки)
|
||||
* 2 = ответ на сообщение (reply)
|
||||
* 3 = репост (repost)
|
||||
* 10 = редактирование (edit) <-- ВАЖНО: как на сервере/в БД-триггере
|
||||
* bodyBytes (BigEndian), новый формат:
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber
|
||||
*
|
||||
* [2] textLenBytes (uint16) — длина текста в байтах UTF-8
|
||||
* [2] textLenBytes (uint16)
|
||||
* [N] text UTF-8
|
||||
*
|
||||
* Далее ТОЛЬКО если subType == 2 или subType == 3 или subType == 10:
|
||||
* Далее ТОЛЬКО если subType == REPLY/REPOST/EDIT:
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [N] toBlockchainName UTF-8
|
||||
* [4] toBlockGlobalNumber (int32)
|
||||
* [32] toBlockHash32 (raw 32 bytes)
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=1
|
||||
*/
|
||||
public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
|
||||
public static final short TYPE = 1;
|
||||
public static final short VER = 1;
|
||||
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
// subType:
|
||||
public static final short SUB_NEW = 1;
|
||||
public static final short SUB_REPLY = 2;
|
||||
public static final short SUB_REPOST = 3;
|
||||
public final short subType; // из header
|
||||
public final short version; // из header
|
||||
|
||||
/** ВАЖНО: EDIT как на сервере (и как ожидает trg_blocks_edit_apply_ai). */
|
||||
public static final short SUB_EDIT = 10;
|
||||
// линейные поля
|
||||
public final int prevLineNumber;
|
||||
public final byte[] prevLineHash32; // 32
|
||||
public final int thisLineNumber;
|
||||
|
||||
/** Подтип текстового сообщения (1/2/3/10). */
|
||||
public final short subType;
|
||||
|
||||
/** Текст сообщения (строго валидный UTF-8, не пустой/не blank). */
|
||||
// payload
|
||||
public final String message;
|
||||
|
||||
// Заполняются только если subType == SUB_REPLY || SUB_REPOST || SUB_EDIT
|
||||
// target (только для reply/repost/edit)
|
||||
public final String toBlockchainName;
|
||||
public final int toBlockGlobalNumber;
|
||||
public final byte[] toBlockHash32;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор из байт =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
/** Десериализация из полного bodyBytes (включая type/version). */
|
||||
public TextBody(byte[] bodyBytes) {
|
||||
public TextBody(short subType, short version, byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
|
||||
// минимум: type+ver (4) + subType(2) + textLen(2)
|
||||
if (bodyBytes.length < 4 + 2 + 2) {
|
||||
this.subType = subType;
|
||||
this.version = version;
|
||||
|
||||
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("TextBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||
}
|
||||
|
||||
if (!isValidSubType(this.subType)) {
|
||||
throw new IllegalArgumentException("Bad Text subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
|
||||
// минимум: line(4+32+4) + textLen(2)
|
||||
if (bodyBytes.length < 4 + 32 + 4 + 2) {
|
||||
throw new IllegalArgumentException("TextBody 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 TextBody: type=" + type + " ver=" + ver);
|
||||
}
|
||||
this.prevLineNumber = bb.getInt();
|
||||
|
||||
this.subType = bb.getShort();
|
||||
if (this.subType != SUB_NEW
|
||||
&& this.subType != SUB_REPLY
|
||||
&& this.subType != SUB_REPOST
|
||||
&& this.subType != SUB_EDIT) {
|
||||
throw new IllegalArgumentException("Bad subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
this.prevLineHash32 = new byte[32];
|
||||
bb.get(this.prevLineHash32);
|
||||
|
||||
this.thisLineNumber = bb.getInt();
|
||||
|
||||
int textLen = Short.toUnsignedInt(bb.getShort());
|
||||
if (textLen <= 0) {
|
||||
throw new IllegalArgumentException("Text payload is empty");
|
||||
}
|
||||
if (bb.remaining() < textLen) {
|
||||
throw new IllegalArgumentException("Text payload too short (len=" + textLen + ")");
|
||||
}
|
||||
if (textLen <= 0) throw new IllegalArgumentException("Text payload is empty");
|
||||
if (bb.remaining() < textLen) throw new IllegalArgumentException("Text payload too short (len=" + textLen + ")");
|
||||
|
||||
byte[] textBytes = new byte[textLen];
|
||||
bb.get(textBytes);
|
||||
|
||||
var decoder = StandardCharsets.UTF_8
|
||||
.newDecoder()
|
||||
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
|
||||
@@ -112,22 +104,16 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
|
||||
}
|
||||
|
||||
if (this.message.isBlank()) {
|
||||
throw new IllegalArgumentException("Text message is blank");
|
||||
}
|
||||
if (this.message.isBlank()) throw new IllegalArgumentException("Text message is blank");
|
||||
|
||||
// Поля ссылки — только для reply/repost/edit
|
||||
if (this.subType == SUB_REPLY || this.subType == SUB_REPOST || this.subType == SUB_EDIT) {
|
||||
|
||||
if (bb.remaining() < 1) {
|
||||
throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
||||
}
|
||||
// target only for reply/repost/edit
|
||||
if (isHasTargetSubType(this.subType)) {
|
||||
if (bb.remaining() < 1) throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
||||
|
||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||
if (bb.remaining() < nameLen + 4 + 32) {
|
||||
if (bb.remaining() < nameLen + 4 + 32)
|
||||
throw new IllegalArgumentException("Reply/Repost/Edit payload too short");
|
||||
}
|
||||
|
||||
byte[] nameBytes = new byte[nameLen];
|
||||
bb.get(nameBytes);
|
||||
@@ -138,110 +124,92 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
this.toBlockHash32 = new byte[32];
|
||||
bb.get(this.toBlockHash32);
|
||||
|
||||
// Запрет мусора в конце
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
|
||||
} else {
|
||||
// SUB_NEW
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = 0;
|
||||
this.toBlockHash32 = null;
|
||||
|
||||
// если кто-то подсунул хвост — лучше упасть, чтобы формат не “плыл”
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail for subType=NEW, remaining=" + bb.remaining());
|
||||
}
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail for subType=NEW, remaining=" + bb.remaining());
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструкторы “для тестов” ====================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
public TextBody(String message) {
|
||||
this(SUB_NEW, message);
|
||||
}
|
||||
|
||||
/** Сообщение subType=NEW (1). */
|
||||
public TextBody(short subType, String message) {
|
||||
Objects.requireNonNull(message, "message == null");
|
||||
|
||||
if (subType != SUB_NEW) {
|
||||
throw new IllegalArgumentException("This constructor is only for SUB_NEW");
|
||||
}
|
||||
if (message.isBlank()) {
|
||||
throw new IllegalArgumentException("message is blank");
|
||||
}
|
||||
|
||||
this.subType = subType;
|
||||
this.message = message;
|
||||
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = 0;
|
||||
this.toBlockHash32 = null;
|
||||
}
|
||||
|
||||
/** Сообщение subType=REPLY (2) или subType=REPOST (3) или subType=EDIT (10) со ссылкой на блок. */
|
||||
public TextBody(short subType,
|
||||
String message,
|
||||
String toBlockchainName,
|
||||
int toBlockGlobalNumber,
|
||||
byte[] toBlockHash32) {
|
||||
public TextBody(int prevLineNumber,
|
||||
byte[] prevLineHash32,
|
||||
int thisLineNumber,
|
||||
short subType,
|
||||
String message,
|
||||
String toBlockchainName,
|
||||
Integer toBlockGlobalNumber,
|
||||
byte[] toBlockHash32) {
|
||||
|
||||
Objects.requireNonNull(message, "message == null");
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
|
||||
if (subType != SUB_REPLY && subType != SUB_REPOST && subType != SUB_EDIT) {
|
||||
throw new IllegalArgumentException("subType must be SUB_REPLY or SUB_REPOST or SUB_EDIT for this constructor");
|
||||
}
|
||||
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.prevLineNumber = prevLineNumber;
|
||||
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
this.thisLineNumber = thisLineNumber;
|
||||
|
||||
this.subType = subType;
|
||||
this.version = VER;
|
||||
|
||||
this.message = message;
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
|
||||
if (isHasTargetSubType(subType)) {
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
if (toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
} else {
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = 0;
|
||||
this.toBlockHash32 = null;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyRecord контракт =========================== */
|
||||
/* ===================================================================== */
|
||||
private static boolean isValidSubType(short st) {
|
||||
int v = st & 0xFFFF;
|
||||
return v == (MsgSubType.TEXT_NEW & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_EDIT & 0xFFFF);
|
||||
}
|
||||
|
||||
@Override public short type() { return TYPE; }
|
||||
@Override public short version() { return VER; }
|
||||
@Override public short subType() { return subType; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return LineIndex.TEXT;
|
||||
private static boolean isHasTargetSubType(short st) {
|
||||
int v = st & 0xFFFF;
|
||||
return v == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_EDIT & 0xFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextBody check() {
|
||||
if (subType != SUB_NEW && subType != SUB_REPLY && subType != SUB_REPOST && subType != SUB_EDIT) {
|
||||
throw new IllegalArgumentException("Bad subType: " + (subType & 0xFFFF));
|
||||
}
|
||||
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||
if (message == null || message.isBlank()) throw new IllegalArgumentException("Text message is blank");
|
||||
|
||||
if (message == null || message.isBlank()) {
|
||||
throw new IllegalArgumentException("Text message is blank");
|
||||
}
|
||||
|
||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank())
|
||||
throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0)
|
||||
throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
// line fields rule:
|
||||
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 (toBlockchainName != null) throw new IllegalArgumentException("toBlockchainName must be null for SUB_NEW");
|
||||
if (toBlockHash32 != null) throw new IllegalArgumentException("toBlockHash32 must be null for SUB_NEW");
|
||||
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||
// thisLineNumber сервер пока не проверяет (принимаем как есть)
|
||||
}
|
||||
|
||||
if (isHasTargetSubType(subType)) {
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank()) throw new IllegalArgumentException("toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 invalid");
|
||||
} else {
|
||||
if (toBlockchainName != null || toBlockHash32 != null) throw new IllegalArgumentException("SUB_NEW must not contain target fields");
|
||||
}
|
||||
|
||||
return this;
|
||||
@@ -250,46 +218,34 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
@Override
|
||||
public byte[] toBytes() {
|
||||
byte[] msgUtf8 = message.getBytes(StandardCharsets.UTF_8);
|
||||
if (msgUtf8.length == 0) {
|
||||
throw new IllegalArgumentException("Text payload is empty");
|
||||
}
|
||||
if (msgUtf8.length > 65535) {
|
||||
throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||
}
|
||||
if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
|
||||
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||
|
||||
// base: type+ver + subType + textLen + textBytes
|
||||
int cap = 4 + 2 + 2 + msgUtf8.length;
|
||||
int cap = 4 + 32 + 4 // line fields
|
||||
+ 2 + msgUtf8.length; // text
|
||||
|
||||
byte[] nameBytes = null;
|
||||
|
||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
||||
if (isHasTargetSubType(subType)) {
|
||||
nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||
if (nameBytes.length == 0 || nameBytes.length > 255) {
|
||||
if (nameBytes.length == 0 || nameBytes.length > 255)
|
||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||
}
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) {
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
}
|
||||
|
||||
cap += 1 + nameBytes.length + 4 + 32;
|
||||
|
||||
} else {
|
||||
if (toBlockchainName != null || toBlockHash32 != null) {
|
||||
throw new IllegalArgumentException("SUB_NEW must not contain reply/repost/edit fields");
|
||||
}
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.putShort(TYPE);
|
||||
bb.putShort(VER);
|
||||
|
||||
bb.putShort(subType);
|
||||
bb.putInt(prevLineNumber);
|
||||
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
bb.putInt(thisLineNumber);
|
||||
|
||||
bb.putShort((short) msgUtf8.length);
|
||||
bb.put(msgUtf8);
|
||||
|
||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
||||
if (isHasTargetSubType(subType)) {
|
||||
bb.put((byte) nameBytes.length);
|
||||
bb.put(nameBytes);
|
||||
bb.putInt(toBlockGlobalNumber);
|
||||
@@ -299,83 +255,32 @@ public final class TextBody implements BodyRecord, BodyHasTarget {
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String st = switch (subType) {
|
||||
case SUB_NEW -> "NEW (1)";
|
||||
case SUB_REPLY -> "REPLY (2)";
|
||||
case SUB_REPOST -> "REPOST (3)";
|
||||
case SUB_EDIT -> "EDIT (10)";
|
||||
default -> "UNKNOWN";
|
||||
};
|
||||
|
||||
if (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) {
|
||||
return """
|
||||
TextBody {
|
||||
тип записи : TEXT (type=1, ver=1)
|
||||
ожидаемая линия : 1
|
||||
subType : %s
|
||||
длина сообщения : %d байт
|
||||
текст сообщения : "%s"
|
||||
ссылка на блок : "%s" #%d
|
||||
hash цели (hex) : %s
|
||||
}
|
||||
""".formatted(
|
||||
st,
|
||||
message.getBytes(StandardCharsets.UTF_8).length,
|
||||
message,
|
||||
toBlockchainName,
|
||||
toBlockGlobalNumber,
|
||||
toBlockHashHex()
|
||||
);
|
||||
}
|
||||
|
||||
return """
|
||||
TextBody {
|
||||
тип записи : TEXT (type=1, ver=1)
|
||||
ожидаемая линия : 1
|
||||
subType : %s
|
||||
длина сообщения : %d байт
|
||||
текст сообщения : "%s"
|
||||
}
|
||||
""".formatted(
|
||||
st,
|
||||
message.getBytes(StandardCharsets.UTF_8).length,
|
||||
message
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
public String toBlockHashHex() {
|
||||
if (toBlockHash32 == null) return "null";
|
||||
char[] HEX = "0123456789abcdef".toCharArray();
|
||||
char[] out = new char[64];
|
||||
for (int i = 0; i < 32; i++) {
|
||||
int v = toBlockHash32[i] & 0xFF;
|
||||
out[i * 2] = HEX[v >>> 4];
|
||||
out[i * 2 + 1] = HEX[v & 0x0F];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
/* ====================== 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; }
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyHasTarget контракт ========================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
/** В формате TextBody login цели не хранится => null. */
|
||||
/* ====================== BodyHasTarget ===================== */
|
||||
@Override public String toLogin() { return null; }
|
||||
|
||||
@Override
|
||||
public String toBchName() {
|
||||
return (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) ? toBlockchainName : null;
|
||||
return isHasTargetSubType(subType) ? toBlockchainName : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer toBlockGlobalNumber() {
|
||||
return (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) ? toBlockGlobalNumber : null;
|
||||
return isHasTargetSubType(subType) ? toBlockGlobalNumber : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBlockHasheBytes() {
|
||||
return (subType == SUB_REPLY || subType == SUB_REPOST || subType == SUB_EDIT) ? toBlockHash32 : null;
|
||||
return isHasTargetSubType(subType) ? toBlockHash32 : null;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,79 @@
|
||||
// =======================
|
||||
// blockchain/body/UserParamBody.java (ИЗМЕНЁННЫЙ: bodyBytes без type/subType/version, + line fields)
|
||||
// =======================
|
||||
package blockchain.body;
|
||||
|
||||
import blockchain.LineIndex;
|
||||
import shine.db.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. (Параметр профиля / данные пользователя о себе)
|
||||
* UserParamBody — type=4, ver=1 (в заголовке блока).
|
||||
*
|
||||
* Идея:
|
||||
* - Это "пользователь сам заявил параметр X со значением Y".
|
||||
* - Один блок = один параметр (одна пара key/value).
|
||||
* (Если нужно больше параметров — просто добавляешь несколько блоков подряд).
|
||||
* subType (в заголовке блока):
|
||||
* 1 = TEXT_TEXT
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [2] type=4
|
||||
* [2] ver=1
|
||||
* bodyBytes (BigEndian), новый формат:
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber
|
||||
*
|
||||
* [2] subType (uint16)
|
||||
* 1 = TEXT_TEXT (ключ-значение, обе строки UTF-8)
|
||||
*
|
||||
* [2] keyLenBytes (uint16) — длина ключа в байтах UTF-8
|
||||
* [2] keyLenBytes (uint16)
|
||||
* [N] keyUtf8
|
||||
*
|
||||
* [2] valueLenBytes (uint16) — длина значения в байтах UTF-8
|
||||
* [2] valueLenBytes (uint16)
|
||||
* [M] valueUtf8
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - длины именно В БАЙТАХ UTF-8 (не в символах)
|
||||
* - ключ и значение обязаны быть валидным UTF-8
|
||||
* - ключ запрещаем пустым/blank (иначе нельзя идентифицировать параметр)
|
||||
* - значение может быть пустым? (реши сам)
|
||||
* сейчас: запрещаем пустое (len>0) и запрещаем blank, чтобы не мусорить цепочку
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=4 (выделенная линия под пользовательские параметры/профиль).
|
||||
*/
|
||||
public final class UserParamBody implements BodyRecord {
|
||||
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);
|
||||
|
||||
// subType:
|
||||
public static final short SUB_TEXT_TEXT = 1;
|
||||
public final short subType; // из header
|
||||
public final short version; // из header
|
||||
|
||||
public final short subType;
|
||||
// line
|
||||
public final int prevLineNumber;
|
||||
public final byte[] prevLineHash32;
|
||||
public final int thisLineNumber;
|
||||
|
||||
/** Название параметра (пример: "firstName", "lastName", "address", "about"). */
|
||||
public final String paramKey;
|
||||
|
||||
/** Значение параметра (пример: "Aidar", "Gareev", "..."). */
|
||||
public final String paramValue;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор из байт =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
public UserParamBody(byte[] bodyBytes) {
|
||||
public UserParamBody(short subType, short version, 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) {
|
||||
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);
|
||||
|
||||
short type = bb.getShort();
|
||||
short ver = bb.getShort();
|
||||
if (type != TYPE || ver != VER) {
|
||||
throw new IllegalArgumentException("Not UserParamBody: type=" + type + " ver=" + ver);
|
||||
}
|
||||
this.prevLineNumber = bb.getInt();
|
||||
|
||||
this.subType = bb.getShort();
|
||||
if (this.subType != SUB_TEXT_TEXT) {
|
||||
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
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");
|
||||
@@ -97,31 +89,30 @@ public final class UserParamBody implements BodyRecord {
|
||||
byte[] valBytes = new byte[valLen];
|
||||
bb.get(valBytes);
|
||||
|
||||
// запрет мусора в конце
|
||||
if (bb.remaining() != 0) {
|
||||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
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");
|
||||
}
|
||||
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) {
|
||||
|
||||
public UserParamBody(String paramKey, String paramValue) {
|
||||
Objects.requireNonNull(paramKey, "paramKey == null");
|
||||
Objects.requireNonNull(paramValue, "paramValue == null");
|
||||
|
||||
this.subType = SUB_TEXT_TEXT;
|
||||
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");
|
||||
@@ -130,55 +121,41 @@ public final class UserParamBody implements BodyRecord {
|
||||
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 LineIndex.USER_PARAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserParamBody check() {
|
||||
if (subType != SUB_TEXT_TEXT)
|
||||
if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF))
|
||||
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");
|
||||
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() {
|
||||
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 == 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");
|
||||
|
||||
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;
|
||||
int cap = (4 + 32 + 4)
|
||||
+ 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.putInt(prevLineNumber);
|
||||
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
bb.putInt(thisLineNumber);
|
||||
|
||||
bb.putShort((short) keyUtf8.length);
|
||||
bb.put(keyUtf8);
|
||||
@@ -189,28 +166,8 @@ public final class UserParamBody implements BodyRecord {
|
||||
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()
|
||||
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
|
||||
@@ -220,4 +177,15 @@ public final class UserParamBody implements BodyRecord {
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user