15 01 25
Доделал типы сообщений посты в линии и едиты на них.ответы на них И ответы в другие блокчейны (Все тесты тесты проходят)
This commit is contained in:
parent
b69075cbac
commit
376d42cd79
@ -9,6 +9,22 @@ package blockchain;
|
||||
*
|
||||
* Важно:
|
||||
* - Значения менять после релиза нельзя (иначе сломается совместимость).
|
||||
*
|
||||
* =========================================================================
|
||||
* Про EDIT-типы (важные правила, чтобы не было “двойных правок”):
|
||||
*
|
||||
* 1) EDIT разрешён ТОЛЬКО автору (в своём блокчейне).
|
||||
* Никаких “я отредачу чужое” — нельзя.
|
||||
*
|
||||
* 2) EDIT всегда ссылается ТОЛЬКО на ОРИГИНАЛ:
|
||||
* - EDIT_POST -> на исходный POST
|
||||
* - EDIT_REPLY -> на исходный REPLY
|
||||
* НЕЛЬЗЯ ссылаться на предыдущий EDIT (цепочка edit-ов запрещена).
|
||||
*
|
||||
* 3) REPLY может ссылаться на блоки в чужих линиях / чужих каналах,
|
||||
* и существование цели на уровне check() не проверяется
|
||||
* (check() БД не видит). Если цели нет — “никто не увидит” и ок.
|
||||
* =========================================================================
|
||||
*/
|
||||
public final class MsgSubType {
|
||||
|
||||
@ -18,20 +34,35 @@ public final class MsgSubType {
|
||||
|
||||
/** HeaderBody: subType всегда 0 (compat). */
|
||||
public static final short HEADER_COMPAT = 0;
|
||||
public static final short TECH_CREATE_CHANNEL = 1;
|
||||
|
||||
/* ===================== TEXT (msg_type=1) ===================== */
|
||||
|
||||
/** Новая публикация. */
|
||||
public static final short TEXT_NEW = 1;
|
||||
/**
|
||||
* POST — обычный пост в канале (в линии канала).
|
||||
* Имеет hasLine (prevLineNumber/prevLineHash32/thisLineNumber).
|
||||
*/
|
||||
public static final short TEXT_POST = 10;
|
||||
|
||||
/** Ответ (reply). */
|
||||
public static final short TEXT_REPLY = 2;
|
||||
/**
|
||||
* EDIT_POST — редактирование ПОСТА.
|
||||
* Имеет hasLine (принадлежит линии канала)
|
||||
* И имеет target на ОРИГИНАЛЬНЫЙ POST (без toBlockchainName).
|
||||
*/
|
||||
public static final short TEXT_EDIT_POST = 11;
|
||||
|
||||
/** Репост (repost). */
|
||||
public static final short TEXT_REPOST = 3;
|
||||
/**
|
||||
* REPLY — ответ на сообщение.
|
||||
* НЕ в линии. Имеет target (toBlockchainName + blockNumber + hash32).
|
||||
* Может указывать на чужой блокчейн/чужую линию/чужой канал.
|
||||
*/
|
||||
public static final short TEXT_REPLY = 20;
|
||||
|
||||
/** Редактирование (edit). */
|
||||
public static final short TEXT_EDIT = 10;
|
||||
/**
|
||||
* EDIT_REPLY — редактирование ОТВЕТА.
|
||||
* НЕ в линии. Имеет target на ОРИГИНАЛЬНЫЙ REPLY (без toBlockchainName).
|
||||
*/
|
||||
public static final short TEXT_EDIT_REPLY = 21;
|
||||
|
||||
/* ===================== REACTION (msg_type=2) ===================== */
|
||||
|
||||
@ -39,27 +70,19 @@ public final class MsgSubType {
|
||||
public static final short REACTION_LIKE = 1;
|
||||
|
||||
/* ===================== CONNECTION (msg_type=3) ===================== */
|
||||
/**
|
||||
* Совпадает с ConnectionBody:
|
||||
* SET: FRIEND=10, CONTACT=20, FOLLOW=30
|
||||
* UNSET: UNFRIEND=11, UNCONTACT=21, UNFOLLOW=31
|
||||
*/
|
||||
|
||||
/** Добавить в друзья. */
|
||||
public static final short CONNECTION_FRIEND = 10;
|
||||
|
||||
/** Удалить из друзей. */
|
||||
public static final short CONNECTION_UNFRIEND = 11;
|
||||
|
||||
/** Добавить в контакты. */
|
||||
public static final short CONNECTION_CONTACT = 20;
|
||||
|
||||
/** Удалить из контактов. */
|
||||
public static final short CONNECTION_UNCONTACT = 21;
|
||||
|
||||
/** Подписаться (follow). */
|
||||
public static final short CONNECTION_FOLLOW = 30;
|
||||
|
||||
/** Отписаться (unfollow). */
|
||||
public static final short CONNECTION_UNFOLLOW = 31;
|
||||
|
||||
@ -67,6 +90,4 @@ public final class MsgSubType {
|
||||
|
||||
/** Параметр профиля key/value (обе строки). */
|
||||
public static final short USER_PARAM_TEXT_TEXT = 1;
|
||||
|
||||
|
||||
}
|
||||
@ -1,13 +1,22 @@
|
||||
package blockchain.body;
|
||||
|
||||
/**
|
||||
* BodyHasLine — для типов, которые имеют линейные поля в body:
|
||||
* TEXT / CONNECTION / USER_PARAM
|
||||
* BodyHasLine — для типов, которые имеют линейные поля в body.
|
||||
*
|
||||
* В проекте hasLine встречается, например, у:
|
||||
* - TECH: CREATE_CHANNEL (type=0, subType=1) — идёт по тех-линии
|
||||
* - TEXT: POST / EDIT_POST (type=1, subType=10/11) — линия канала
|
||||
* - CONNECTION (type=3)
|
||||
* - USER_PARAM (type=4)
|
||||
*
|
||||
* Формат линейных полей (BigEndian) в НАЧАЛЕ bodyBytes:
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber
|
||||
*
|
||||
* Важно:
|
||||
* - Правильность prevLineNumber/hash и согласование thisLineNumber
|
||||
* проверяется на сервере/в БД при вставке (а не в body.check()).
|
||||
*/
|
||||
public interface BodyHasLine {
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package blockchain.body;
|
||||
|
||||
/**
|
||||
* Парсер body теперь выбирает класс по header: type/subType/version,
|
||||
* Парсер body выбирает класс по header: type/subType/version,
|
||||
* потому что bodyBytes больше НЕ содержат type/subType/version.
|
||||
*/
|
||||
public final class BodyRecordParser {
|
||||
@ -14,23 +14,31 @@ public final class BodyRecordParser {
|
||||
int t = type & 0xFFFF;
|
||||
int v = version & 0xFFFF;
|
||||
|
||||
// ключ = (type<<16)|version (как раньше по смыслу), но берём из HEADER
|
||||
int key = (t << 16) | v;
|
||||
|
||||
BodyRecord r = switch (key) {
|
||||
case HeaderBody.KEY -> new HeaderBody(subType, version, bodyBytes);
|
||||
case HeaderBody.KEY -> {
|
||||
int st = subType & 0xFFFF;
|
||||
if (st == (HeaderBody.SUBTYPE_COMPAT & 0xFFFF)) {
|
||||
yield new HeaderBody(subType, version, bodyBytes);
|
||||
}
|
||||
if (st == (CreateChannelBody.SUBTYPE & 0xFFFF)) {
|
||||
yield new CreateChannelBody(subType, version, bodyBytes);
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown TECH subType for type=0 ver=1: subType=" + st);
|
||||
}
|
||||
|
||||
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 from header: type=%d ver=%d subType=%d",
|
||||
t, v, (subType & 0xFFFF)
|
||||
));
|
||||
};
|
||||
|
||||
// 1) “построили” объект
|
||||
// 2) ОБЯЗАТЕЛЬНО прогнали валидацию
|
||||
return r.check();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
package blockchain.body;
|
||||
|
||||
import blockchain.MsgSubType;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* CreateChannelBody — TECH сообщение создания канала.
|
||||
*
|
||||
* type=0, ver=1 (в заголовке блока)
|
||||
* subType=MsgSubType.TECH_CREATE_CHANNEL (=1)
|
||||
*
|
||||
* Это сообщение идёт по ТЕХ-ЛИНИИ (hasLine):
|
||||
* - prevLineNumber/hash указывают на предыдущее TECH-сообщение (HEADER или прошлый CREATE_CHANNEL)
|
||||
* - thisLineNumber: 1,2,3... (тех-нумерация)
|
||||
*
|
||||
* bodyBytes (BigEndian):
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber
|
||||
* [1] channelNameLen (uint8)
|
||||
* [N] channelName UTF-8 (^[A-Za-z0-9_]+$)
|
||||
*
|
||||
* Важно:
|
||||
* - канал "0" зарезервирован (создаётся по умолчанию от HEADER), создавать его нельзя.
|
||||
*/
|
||||
public final class CreateChannelBody implements BodyRecord, BodyHasLine {
|
||||
|
||||
public static final short TYPE = 0;
|
||||
public static final short VER = 1;
|
||||
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
public static final short SUBTYPE = MsgSubType.TECH_CREATE_CHANNEL;
|
||||
|
||||
private static final byte[] ZERO32 = new byte[32];
|
||||
|
||||
public final short subType; // из header
|
||||
public final short version; // из header
|
||||
|
||||
// line
|
||||
public final int prevLineNumber;
|
||||
public final byte[] prevLineHash32; // 32
|
||||
public final int thisLineNumber;
|
||||
|
||||
// payload
|
||||
public final String channelName;
|
||||
|
||||
public CreateChannelBody(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("CreateChannelBody version must be 1, got=" + (this.version & 0xFFFF));
|
||||
}
|
||||
if ((this.subType & 0xFFFF) != (SUBTYPE & 0xFFFF)) {
|
||||
throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1), got=" + (this.subType & 0xFFFF));
|
||||
}
|
||||
|
||||
if (bodyBytes.length < (4 + 32 + 4) + 1 + 1) {
|
||||
throw new IllegalArgumentException("CreateChannelBody too short");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
this.prevLineNumber = bb.getInt();
|
||||
|
||||
this.prevLineHash32 = new byte[32];
|
||||
bb.get(this.prevLineHash32);
|
||||
|
||||
this.thisLineNumber = bb.getInt();
|
||||
|
||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("channelNameLen is 0");
|
||||
if (bb.remaining() != nameLen) {
|
||||
throw new IllegalArgumentException("CreateChannelBody tail mismatch: remaining=" + bb.remaining() + " nameLen=" + nameLen);
|
||||
}
|
||||
|
||||
byte[] nameBytes = new byte[nameLen];
|
||||
bb.get(nameBytes);
|
||||
|
||||
this.channelName = new String(nameBytes, StandardCharsets.UTF_8);
|
||||
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
}
|
||||
|
||||
public CreateChannelBody(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String channelName) {
|
||||
Objects.requireNonNull(channelName, "channelName == null");
|
||||
|
||||
this.subType = SUBTYPE;
|
||||
this.version = VER;
|
||||
|
||||
this.prevLineNumber = prevLineNumber;
|
||||
this.prevLineHash32 = (prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
|
||||
this.thisLineNumber = thisLineNumber;
|
||||
|
||||
this.channelName = channelName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateChannelBody check() {
|
||||
if ((subType & 0xFFFF) != (SUBTYPE & 0xFFFF))
|
||||
throw new IllegalArgumentException("CreateChannelBody subType must be TECH_CREATE_CHANNEL(1)");
|
||||
|
||||
if (channelName == null || channelName.isBlank())
|
||||
throw new IllegalArgumentException("channelName is blank");
|
||||
|
||||
if (!channelName.matches("^[A-Za-z0-9_]+$"))
|
||||
throw new IllegalArgumentException("channelName must match ^[A-Za-z0-9_]+$");
|
||||
|
||||
if ("0".equals(channelName))
|
||||
throw new IllegalArgumentException("channelName \"0\" is reserved");
|
||||
|
||||
// tech-line: prev обязателен (минимум HEADER=0)
|
||||
if (prevLineNumber < 0)
|
||||
throw new IllegalArgumentException("prevLineNumber must be >=0 for CreateChannelBody");
|
||||
if (prevLineHash32 == null || prevLineHash32.length != 32)
|
||||
throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||
if (thisLineNumber <= 0)
|
||||
throw new IllegalArgumentException("thisLineNumber must be >=1 for CreateChannelBody");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBytes() {
|
||||
byte[] nameUtf8 = channelName.getBytes(StandardCharsets.UTF_8);
|
||||
if (nameUtf8.length == 0 || nameUtf8.length > 255)
|
||||
throw new IllegalArgumentException("channelName utf8 len must be 1..255");
|
||||
|
||||
int cap = (4 + 32 + 4) + 1 + nameUtf8.length;
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.putInt(prevLineNumber);
|
||||
bb.put(prevLineHash32 == null ? ZERO32 : Arrays.copyOf(prevLineHash32, 32));
|
||||
bb.putInt(thisLineNumber);
|
||||
|
||||
bb.put((byte) nameUtf8.length);
|
||||
bb.put(nameUtf8);
|
||||
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
/* ====================== 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; }
|
||||
}
|
||||
@ -14,24 +14,74 @@ import java.util.Objects;
|
||||
* TextBody — type=1, ver=1 (в заголовке блока).
|
||||
*
|
||||
* subType (в заголовке блока):
|
||||
* 1 = NEW
|
||||
* 2 = REPLY
|
||||
* 3 = REPOST
|
||||
* 10 = EDIT
|
||||
* 10 = POST
|
||||
* 11 = EDIT_POST
|
||||
* 20 = REPLY
|
||||
* 21 = EDIT_REPLY
|
||||
*
|
||||
* bodyBytes (BigEndian), новый формат:
|
||||
* =========================================================================
|
||||
* КОНЦЕПЦИЯ ЛИНИЙ ДЛЯ ТЕКСТОВЫХ СООБЩЕНИЙ:
|
||||
*
|
||||
* POST и EDIT_POST принадлежат ЛИНИИ КАНАЛА и имеют hasLine:
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber
|
||||
*
|
||||
* Канал в POST/EDIT_POST НЕ хранится (channelName не лежит в bodyBytes).
|
||||
* Канал определяется логически через lineRootBlockNumber:
|
||||
* - канал "0": lineRootBlockNumber = blockNumber заголовка (HEADER)
|
||||
* - канал "X": lineRootBlockNumber = blockNumber тех-сообщения CREATE_CHANNEL("X")
|
||||
*
|
||||
* REPLY и EDIT_REPLY НЕ имеют линии (нет hasLine).
|
||||
*
|
||||
* =========================================================================
|
||||
* ФОРМАТЫ bodyBytes (BigEndian):
|
||||
*
|
||||
* 1) POST (subType=10):
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber // 0,1,2...
|
||||
* [2] textLenBytes (uint16)
|
||||
* [N] text UTF-8
|
||||
*
|
||||
* Далее ТОЛЬКО если subType == REPLY/REPOST/EDIT:
|
||||
* 2) EDIT_POST (subType=11):
|
||||
* [4] prevLineNumber
|
||||
* [32] prevLineHash32
|
||||
* [4] thisLineNumber // равен thisLineNumber предыдущего сообщения линии
|
||||
*
|
||||
* hasTarget (на ОРИГИНАЛЬНЫЙ POST, toBchName НЕ хранить):
|
||||
* [4] toBlockGlobalNumber
|
||||
* [32] toBlockHash32
|
||||
*
|
||||
* [2] textLenBytes (uint16)
|
||||
* [N] text UTF-8
|
||||
*
|
||||
* 3) REPLY (subType=20) — НЕ в линии:
|
||||
* hasTarget (может быть на чужой блокчейн; существование НЕ проверяем):
|
||||
* [1] toBlockchainNameLen (uint8)
|
||||
* [N] toBlockchainName UTF-8
|
||||
* [4] toBlockGlobalNumber (int32)
|
||||
* [32] toBlockHash32 (raw 32 bytes)
|
||||
* [4] toBlockGlobalNumber
|
||||
* [32] toBlockHash32
|
||||
*
|
||||
* [2] textLenBytes (uint16)
|
||||
* [M] text UTF-8
|
||||
*
|
||||
* 4) EDIT_REPLY (subType=21) — НЕ в линии:
|
||||
* hasTarget (на ОРИГИНАЛЬНЫЙ REPLY, toBchName НЕ хранить):
|
||||
* [4] toBlockGlobalNumber
|
||||
* [32] toBlockHash32
|
||||
*
|
||||
* [2] textLenBytes (uint16)
|
||||
* [N] text UTF-8
|
||||
*
|
||||
* =========================================================================
|
||||
* ВАЖНО:
|
||||
* - Body.check() НЕ имеет доступа к БД, поэтому:
|
||||
* - не проверяет существование prevLineNumber/hash
|
||||
* - не проверяет согласование thisLineNumber относительно prev
|
||||
* - не проверяет существование target для REPLY
|
||||
*
|
||||
* Эти проверки выполняются на сервере/в БД при вставке.
|
||||
*/
|
||||
public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
|
||||
@ -43,18 +93,25 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
public final short subType; // из header
|
||||
public final short version; // из header
|
||||
|
||||
// линейные поля
|
||||
// ===== line fields (только для POST/EDIT_POST) =====
|
||||
// Для REPLY/EDIT_REPLY эти поля НЕ сериализуются; значения держим как "пустые".
|
||||
public final int prevLineNumber;
|
||||
public final byte[] prevLineHash32; // 32
|
||||
public final byte[] prevLineHash32; // 32 or null
|
||||
public final int thisLineNumber;
|
||||
|
||||
// payload
|
||||
// ===== message text =====
|
||||
public final String message;
|
||||
|
||||
// target (только для reply/repost/edit)
|
||||
public final String toBlockchainName;
|
||||
public final int toBlockGlobalNumber;
|
||||
public final byte[] toBlockHash32;
|
||||
// ===== target fields =====
|
||||
// REPLY: toBlockchainName + globalNumber + hash32
|
||||
// EDIT_POST / EDIT_REPLY: только globalNumber + hash32 (без toBlockchainName)
|
||||
public final String toBlockchainName; // nullable
|
||||
public final Integer toBlockGlobalNumber; // nullable
|
||||
public final byte[] toBlockHash32; // nullable(но если target есть -> 32)
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор из байт ========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
public TextBody(short subType, short version, byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
@ -65,52 +122,59 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
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);
|
||||
|
||||
this.prevLineNumber = bb.getInt();
|
||||
int st = this.subType & 0xFFFF;
|
||||
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
|
||||
// POST: hasLine + text
|
||||
ensureMin(bb, (4 + 32 + 4) + 2, "POST too short");
|
||||
|
||||
this.prevLineNumber = bb.getInt();
|
||||
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 + ")");
|
||||
this.message = readStrictUtf8Len16(bb, "POST text");
|
||||
|
||||
byte[] textBytes = new byte[textLen];
|
||||
bb.get(textBytes);
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = null;
|
||||
this.toBlockHash32 = null;
|
||||
|
||||
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
ensureNoTail(bb, "POST");
|
||||
|
||||
try {
|
||||
this.message = decoder.decode(ByteBuffer.wrap(textBytes)).toString();
|
||||
} catch (CharacterCodingException e) {
|
||||
throw new IllegalArgumentException("Text payload is not valid UTF-8", e);
|
||||
}
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
// EDIT_POST: hasLine + target(no bch) + text
|
||||
ensureMin(bb, (4 + 32 + 4) + (4 + 32) + 2, "EDIT_POST too short");
|
||||
|
||||
if (this.message.isBlank()) throw new IllegalArgumentException("Text message is blank");
|
||||
this.prevLineNumber = bb.getInt();
|
||||
this.prevLineHash32 = new byte[32];
|
||||
bb.get(this.prevLineHash32);
|
||||
this.thisLineNumber = bb.getInt();
|
||||
|
||||
// target only for reply/repost/edit
|
||||
if (isHasTargetSubType(this.subType)) {
|
||||
if (bb.remaining() < 1) throw new IllegalArgumentException("Missing toBlockchainNameLen");
|
||||
int tgtNum = bb.getInt();
|
||||
byte[] tgtHash = new byte[32];
|
||||
bb.get(tgtHash);
|
||||
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = tgtNum;
|
||||
this.toBlockHash32 = tgtHash;
|
||||
|
||||
this.message = readStrictUtf8Len16(bb, "EDIT_POST text");
|
||||
|
||||
ensureNoTail(bb, "EDIT_POST");
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||
// REPLY: target(with bch) + text
|
||||
ensureMin(bb, 1 + 1 + 4 + 32 + 2, "REPLY too short");
|
||||
|
||||
int nameLen = Byte.toUnsignedInt(bb.get());
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("toBlockchainNameLen is 0");
|
||||
if (bb.remaining() < nameLen + 4 + 32)
|
||||
throw new IllegalArgumentException("Reply/Repost/Edit payload too short");
|
||||
if (nameLen <= 0) throw new IllegalArgumentException("REPLY toBlockchainNameLen is 0");
|
||||
ensureMin(bb, nameLen + 4 + 32 + 2, "REPLY payload too short");
|
||||
|
||||
byte[] nameBytes = new byte[nameLen];
|
||||
bb.get(nameBytes);
|
||||
@ -121,40 +185,121 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
this.toBlockHash32 = new byte[32];
|
||||
bb.get(this.toBlockHash32);
|
||||
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||||
this.message = readStrictUtf8Len16(bb, "REPLY text");
|
||||
|
||||
// line fields отсутствуют в байтах
|
||||
this.prevLineNumber = -1;
|
||||
this.prevLineHash32 = null;
|
||||
this.thisLineNumber = -1;
|
||||
|
||||
ensureNoTail(bb, "REPLY");
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||
// EDIT_REPLY: target(no bch) + text
|
||||
ensureMin(bb, (4 + 32) + 2, "EDIT_REPLY too short");
|
||||
|
||||
int tgtNum = bb.getInt();
|
||||
byte[] tgtHash = new byte[32];
|
||||
bb.get(tgtHash);
|
||||
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = tgtNum;
|
||||
this.toBlockHash32 = tgtHash;
|
||||
|
||||
this.message = readStrictUtf8Len16(bb, "EDIT_REPLY text");
|
||||
|
||||
// line fields отсутствуют в байтах
|
||||
this.prevLineNumber = -1;
|
||||
this.prevLineHash32 = null;
|
||||
this.thisLineNumber = -1;
|
||||
|
||||
ensureNoTail(bb, "EDIT_REPLY");
|
||||
|
||||
} else {
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = 0;
|
||||
this.toBlockHash32 = null;
|
||||
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail for subType=NEW, remaining=" + bb.remaining());
|
||||
// недостижимо из-за isValidSubType, но пусть будет
|
||||
throw new IllegalArgumentException("Unsupported Text subType: " + st);
|
||||
}
|
||||
}
|
||||
|
||||
public TextBody(int prevLineNumber,
|
||||
/* ===================================================================== */
|
||||
/* ====================== Фабрики (удобно) ============================= */
|
||||
/* ===================================================================== */
|
||||
|
||||
public static TextBody newPost(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber, String message) {
|
||||
return new TextBody(MsgSubType.TEXT_POST, prevLineNumber, prevLineHash32, thisLineNumber,
|
||||
message, null, null, null);
|
||||
}
|
||||
|
||||
public static TextBody newEditPost(int prevLineNumber, byte[] prevLineHash32, int thisLineNumber,
|
||||
int targetBlockNumber, byte[] targetHash32,
|
||||
String message) {
|
||||
return new TextBody(MsgSubType.TEXT_EDIT_POST, prevLineNumber, prevLineHash32, thisLineNumber,
|
||||
message, null, targetBlockNumber, targetHash32);
|
||||
}
|
||||
|
||||
public static TextBody newReply(String toBlockchainName, int targetBlockNumber, byte[] targetHash32, String message) {
|
||||
return new TextBody(MsgSubType.TEXT_REPLY, -1, null, -1,
|
||||
message, toBlockchainName, targetBlockNumber, targetHash32);
|
||||
}
|
||||
|
||||
public static TextBody newEditReply(int targetBlockNumber, byte[] targetHash32, String message) {
|
||||
return new TextBody(MsgSubType.TEXT_EDIT_REPLY, -1, null, -1,
|
||||
message, null, targetBlockNumber, targetHash32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Универсальный конструктор “вручную”.
|
||||
* Для REPLY/EDIT_REPLY line поля игнорируются при сериализации (их в формате нет).
|
||||
*/
|
||||
public TextBody(short subType,
|
||||
int prevLineNumber,
|
||||
byte[] prevLineHash32,
|
||||
int thisLineNumber,
|
||||
short subType,
|
||||
String message,
|
||||
String toBlockchainName,
|
||||
Integer toBlockGlobalNumber,
|
||||
byte[] toBlockHash32) {
|
||||
|
||||
Objects.requireNonNull(message, "message == null");
|
||||
|
||||
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||
if (message.isBlank()) throw new IllegalArgumentException("message is blank");
|
||||
|
||||
this.prevLineNumber = prevLineNumber;
|
||||
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
this.thisLineNumber = thisLineNumber;
|
||||
|
||||
this.subType = subType;
|
||||
this.version = VER;
|
||||
|
||||
int st = subType & 0xFFFF;
|
||||
|
||||
// line применима только к POST/EDIT_POST
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
this.prevLineNumber = prevLineNumber;
|
||||
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
this.thisLineNumber = thisLineNumber;
|
||||
} else {
|
||||
this.prevLineNumber = -1;
|
||||
this.prevLineHash32 = null;
|
||||
this.thisLineNumber = -1;
|
||||
}
|
||||
|
||||
this.message = message;
|
||||
|
||||
if (isHasTargetSubType(subType)) {
|
||||
// target правила
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = null;
|
||||
this.toBlockHash32 = null;
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.toBlockchainName = null; // по ТЗ: не хранить
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||||
Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
@ -165,47 +310,81 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
this.toBlockchainName = toBlockchainName;
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||
Objects.requireNonNull(toBlockGlobalNumber, "toBlockGlobalNumber == null");
|
||||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||||
if (toBlockGlobalNumber < 0) throw new IllegalArgumentException("toBlockGlobalNumber < 0");
|
||||
if (toBlockHash32.length != 32) throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
this.toBlockchainName = null; // по ТЗ: не хранить
|
||||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||||
|
||||
} else {
|
||||
// недостижимо
|
||||
this.toBlockchainName = null;
|
||||
this.toBlockGlobalNumber = 0;
|
||||
this.toBlockGlobalNumber = null;
|
||||
this.toBlockHash32 = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isValidSubType(short st) {
|
||||
int v = st & 0xFFFF;
|
||||
return v == (MsgSubType.TEXT_NEW & 0xFFFF)
|
||||
return v == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_REPOST & 0xFFFF)
|
||||
|| v == (MsgSubType.TEXT_EDIT & 0xFFFF);
|
||||
}
|
||||
|
||||
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);
|
||||
|| v == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextBody check() {
|
||||
if (!isValidSubType(subType)) throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||
if (message == null || message.isBlank()) throw new IllegalArgumentException("Text message is blank");
|
||||
if (!isValidSubType(subType))
|
||||
throw new IllegalArgumentException("Bad Text subType: " + (subType & 0xFFFF));
|
||||
|
||||
// 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");
|
||||
if (message == null || message.isBlank())
|
||||
throw new IllegalArgumentException("Text message is blank");
|
||||
|
||||
int st = subType & 0xFFFF;
|
||||
|
||||
// локальные проверки line (БД не трогаем)
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF) || st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
if (prevLineHash32 == null || prevLineHash32.length != 32)
|
||||
throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||
} else {
|
||||
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
||||
// reply/edit_reply: line отсутствует
|
||||
if (prevLineHash32 != null)
|
||||
throw new IllegalArgumentException("REPLY/EDIT_REPLY must not contain line hash");
|
||||
}
|
||||
|
||||
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");
|
||||
// target rules
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
|
||||
if (toBlockchainName != null || toBlockGlobalNumber != null || toBlockHash32 != null)
|
||||
throw new IllegalArgumentException("POST must not contain target fields");
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
if (toBlockchainName != null)
|
||||
throw new IllegalArgumentException("EDIT_POST must not contain toBlockchainName in target");
|
||||
if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
|
||||
throw new IllegalArgumentException("EDIT_POST toBlockGlobalNumber invalid");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("EDIT_POST toBlockHash32 invalid");
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||
if (toBlockchainName == null || toBlockchainName.isBlank())
|
||||
throw new IllegalArgumentException("REPLY toBlockchainName is blank");
|
||||
if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
|
||||
throw new IllegalArgumentException("REPLY toBlockGlobalNumber invalid");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("REPLY toBlockHash32 invalid");
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||
if (toBlockchainName != null)
|
||||
throw new IllegalArgumentException("EDIT_REPLY must not contain toBlockchainName in target");
|
||||
if (toBlockGlobalNumber == null || toBlockGlobalNumber < 0)
|
||||
throw new IllegalArgumentException("EDIT_REPLY toBlockGlobalNumber invalid");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("EDIT_REPLY toBlockHash32 invalid");
|
||||
}
|
||||
|
||||
return this;
|
||||
@ -217,53 +396,152 @@ public final class TextBody implements BodyRecord, BodyHasTarget, BodyHasLine {
|
||||
if (msgUtf8.length == 0) throw new IllegalArgumentException("Text payload is empty");
|
||||
if (msgUtf8.length > 65535) throw new IllegalArgumentException("Text too long (>65535 bytes)");
|
||||
|
||||
int cap = 4 + 32 + 4
|
||||
+ 2 + msgUtf8.length;
|
||||
int st = subType & 0xFFFF;
|
||||
|
||||
byte[] nameBytes = null;
|
||||
|
||||
if (isHasTargetSubType(subType)) {
|
||||
nameBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||
if (nameBytes.length == 0 || nameBytes.length > 255)
|
||||
throw new IllegalArgumentException("toBlockchainName utf8 len must be 1..255");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32)
|
||||
throw new IllegalArgumentException("toBlockHash32 != 32");
|
||||
|
||||
cap += 1 + nameBytes.length + 4 + 32;
|
||||
}
|
||||
if (st == (MsgSubType.TEXT_POST & 0xFFFF)) {
|
||||
// hasLine + text
|
||||
int cap = (4 + 32 + 4) + 2 + msgUtf8.length;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
bb.putInt(prevLineNumber);
|
||||
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
bb.putInt(thisLineNumber);
|
||||
bb.putShort((short) msgUtf8.length);
|
||||
bb.put(msgUtf8);
|
||||
return bb.array();
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)) {
|
||||
// hasLine + target(no bch) + text
|
||||
if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_POST missing toBlockGlobalNumber");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_POST toBlockHash32 != 32");
|
||||
|
||||
int cap = (4 + 32 + 4) + (4 + 32) + 2 + msgUtf8.length;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
bb.putInt(prevLineNumber);
|
||||
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
||||
bb.putInt(thisLineNumber);
|
||||
|
||||
bb.putShort((short) msgUtf8.length);
|
||||
bb.put(msgUtf8);
|
||||
|
||||
if (isHasTargetSubType(subType)) {
|
||||
bb.put((byte) nameBytes.length);
|
||||
bb.put(nameBytes);
|
||||
bb.putInt(toBlockGlobalNumber);
|
||||
bb.put(toBlockHash32);
|
||||
}
|
||||
|
||||
bb.putShort((short) msgUtf8.length);
|
||||
bb.put(msgUtf8);
|
||||
return bb.array();
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_REPLY & 0xFFFF)) {
|
||||
// target(with bch) + text
|
||||
if (toBlockchainName == null) throw new IllegalArgumentException("REPLY missing toBlockchainName");
|
||||
if (toBlockGlobalNumber == null) throw new IllegalArgumentException("REPLY missing toBlockGlobalNumber");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("REPLY toBlockHash32 != 32");
|
||||
|
||||
byte[] nameUtf8 = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||||
if (nameUtf8.length == 0 || nameUtf8.length > 255)
|
||||
throw new IllegalArgumentException("REPLY toBlockchainName utf8 len must be 1..255");
|
||||
|
||||
int cap = 1 + nameUtf8.length + 4 + 32
|
||||
+ 2 + msgUtf8.length;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
bb.put((byte) nameUtf8.length);
|
||||
bb.put(nameUtf8);
|
||||
bb.putInt(toBlockGlobalNumber);
|
||||
bb.put(toBlockHash32);
|
||||
|
||||
bb.putShort((short) msgUtf8.length);
|
||||
bb.put(msgUtf8);
|
||||
return bb.array();
|
||||
|
||||
} else if (st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF)) {
|
||||
// target(no bch) + text
|
||||
if (toBlockGlobalNumber == null) throw new IllegalArgumentException("EDIT_REPLY missing toBlockGlobalNumber");
|
||||
if (toBlockHash32 == null || toBlockHash32.length != 32) throw new IllegalArgumentException("EDIT_REPLY toBlockHash32 != 32");
|
||||
|
||||
int cap = (4 + 32) + 2 + msgUtf8.length;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
bb.putInt(toBlockGlobalNumber);
|
||||
bb.put(toBlockHash32);
|
||||
|
||||
bb.putShort((short) msgUtf8.length);
|
||||
bb.put(msgUtf8);
|
||||
return bb.array();
|
||||
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported Text subType: " + st);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
/* ===================================================================== */
|
||||
/* ========================== Helpers ================================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
private static String readStrictUtf8Len16(ByteBuffer bb, String fieldName) {
|
||||
int len = Short.toUnsignedInt(bb.getShort());
|
||||
if (len <= 0) throw new IllegalArgumentException(fieldName + " is empty");
|
||||
if (bb.remaining() < len) throw new IllegalArgumentException(fieldName + " payload too short (len=" + len + ")");
|
||||
|
||||
byte[] bytes = new byte[len];
|
||||
bb.get(bytes);
|
||||
|
||||
var decoder = StandardCharsets.UTF_8.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPORT)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
||||
|
||||
try {
|
||||
String s = decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
||||
if (s.isBlank()) throw new IllegalArgumentException(fieldName + " is blank");
|
||||
return s;
|
||||
} catch (CharacterCodingException e) {
|
||||
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureMin(ByteBuffer bb, int need, String msg) {
|
||||
if (bb.remaining() < need) throw new IllegalArgumentException(msg + " (need=" + need + ", remaining=" + bb.remaining() + ")");
|
||||
}
|
||||
|
||||
private static void ensureNoTail(ByteBuffer bb, String ctx) {
|
||||
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes for " + ctx + ", remaining=" + bb.remaining());
|
||||
}
|
||||
|
||||
/* ====================== BodyHasLine ====================== */
|
||||
@Override public int prevLineNumber() { return prevLineNumber; }
|
||||
@Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
|
||||
@Override public byte[] prevLineHash32() {
|
||||
if (prevLineHash32 == null) return null;
|
||||
return Arrays.copyOf(prevLineHash32, 32);
|
||||
}
|
||||
@Override public int thisLineNumber() { return thisLineNumber; }
|
||||
|
||||
/* ====================== BodyHasTarget ===================== */
|
||||
@Override public String toBchName() { return isHasTargetSubType(subType) ? toBlockchainName : null; }
|
||||
@Override public Integer toBlockGlobalNumber() { return isHasTargetSubType(subType) ? toBlockGlobalNumber : null; }
|
||||
@Override public byte[] toBlockHashBytes() { return isHasTargetSubType(subType) ? toBlockHash32 : null; }
|
||||
@Override public String toBchName() { return toBlockchainName; }
|
||||
@Override public Integer toBlockGlobalNumber() { return toBlockGlobalNumber; }
|
||||
@Override public byte[] toBlockHashBytes() { return toBlockHash32; }
|
||||
|
||||
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ===================== Удобные хелперы (для ChainState) =============== */
|
||||
/* ===================================================================== */
|
||||
|
||||
/** true только для POST / EDIT_POST (т.е. это сообщение в линии канала). */
|
||||
public boolean isLineMessage() {
|
||||
int st = subType & 0xFFFF;
|
||||
return st == (MsgSubType.TEXT_POST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF);
|
||||
}
|
||||
|
||||
/** true только для EDIT_POST / EDIT_REPLY. */
|
||||
public boolean isEditMessage() {
|
||||
int st = subType & 0xFFFF;
|
||||
return st == (MsgSubType.TEXT_EDIT_POST & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
|
||||
}
|
||||
|
||||
/** true только для REPLY / EDIT_REPLY (т.е. “не в линии”). */
|
||||
public boolean isReplyFamily() {
|
||||
int st = subType & 0xFFFF;
|
||||
return st == (MsgSubType.TEXT_REPLY & 0xFFFF)
|
||||
|| st == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF);
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
* - signature = Ed25519.sign(hash32)
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - Линии (prevLine/thisLine) по ТЗ нужны только для TEXT/CONNECTION/USER_PARAM.
|
||||
* - Здесь НЕТ обращения к blockchain.LineIndex.
|
||||
* - Линии по ТЗ ведём на стороне сервера/БД (триггеры), а в тестах считаем локально.
|
||||
*/
|
||||
public final class AddBlockSender {
|
||||
|
||||
@ -75,7 +74,6 @@ public final class AddBlockSender {
|
||||
|
||||
byte[] bodyBytes = body.toBytes();
|
||||
|
||||
// preimage -> hash32 -> signature
|
||||
byte[] preimage = buildPreimage(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes);
|
||||
byte[] hash32 = blockchain.BchCryptoVerifier.sha256(preimage);
|
||||
byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);
|
||||
@ -102,7 +100,6 @@ public final class AddBlockSender {
|
||||
|
||||
String serverLastHash = JsonMini.extractPayloadString(resp, "serverLastBlockHash");
|
||||
if (serverLastHash == null) {
|
||||
// на всякий случай, но ты говорил старое не поддерживаем — оставил мягко
|
||||
serverLastHash = JsonMini.extractPayloadString(resp, "serverLastGlobalHash");
|
||||
}
|
||||
|
||||
@ -118,15 +115,7 @@ public final class AddBlockSender {
|
||||
|
||||
assertEquals(localHashHex, serverLastHash, op + ": serverLastBlockHash must match local hash");
|
||||
|
||||
// фиксируем в state глобальную цепочку + (если нужно) line-state по TYPE
|
||||
state.applyAppendedBlock(blockNumber, entry.getHash32(), isHeader, type);
|
||||
|
||||
// если это line-body — обновим thisLineNumber в state (для nextLineByType())
|
||||
if (body instanceof BodyHasLine hl) {
|
||||
if (ChainState.isLineType(type)) {
|
||||
state.applyThisLineNumberByType(type, hl.thisLineNumber());
|
||||
}
|
||||
}
|
||||
state.applyAppendedBlock(blockNumber, entry.getHash32(), isHeader, type, body);
|
||||
|
||||
if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
|
||||
}
|
||||
@ -174,6 +163,7 @@ public final class AddBlockSender {
|
||||
|
||||
private static short typeOf(BodyRecord body) {
|
||||
if (body instanceof HeaderBody) return HeaderBody.TYPE;
|
||||
if (body instanceof CreateChannelBody) return CreateChannelBody.TYPE;
|
||||
if (body instanceof TextBody) return TextBody.TYPE;
|
||||
if (body instanceof ReactionBody) return ReactionBody.TYPE;
|
||||
if (body instanceof ConnectionBody) return ConnectionBody.TYPE;
|
||||
@ -183,6 +173,7 @@ public final class AddBlockSender {
|
||||
|
||||
private static short subTypeOf(BodyRecord body) {
|
||||
if (body instanceof HeaderBody hb) return hb.subType;
|
||||
if (body instanceof CreateChannelBody cb) return cb.subType;
|
||||
if (body instanceof TextBody tb) return tb.subType;
|
||||
if (body instanceof ReactionBody rb) return rb.subType;
|
||||
if (body instanceof ConnectionBody cb) return cb.subType;
|
||||
@ -192,6 +183,7 @@ public final class AddBlockSender {
|
||||
|
||||
private static short versionOf(BodyRecord body) {
|
||||
if (body instanceof HeaderBody hb) return hb.version;
|
||||
if (body instanceof CreateChannelBody cb) return cb.version;
|
||||
if (body instanceof TextBody tb) return tb.version;
|
||||
if (body instanceof ReactionBody rb) return rb.version;
|
||||
if (body instanceof ConnectionBody cb) return cb.version;
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
package test.it.blockchain;
|
||||
|
||||
import java.util.Arrays;
|
||||
import blockchain.MsgSubType;
|
||||
import blockchain.body.BodyRecord;
|
||||
import blockchain.body.BodyHasLine;
|
||||
import blockchain.body.CreateChannelBody;
|
||||
import blockchain.body.TextBody;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ChainState — состояние глобальной цепочки + состояние линий (только тех, где они нужны).
|
||||
* ChainState — состояние глобальной цепочки + состояние линий.
|
||||
*
|
||||
* Глобальная цепочка:
|
||||
* - lastBlockNumber / lastBlockHashHex
|
||||
* - map blockNumber -> hash32 (для ссылок reply/edit/reaction)
|
||||
* - map blockNumber -> hash32
|
||||
*
|
||||
* Линии по ТЗ нужны только для:
|
||||
* - TEXT (type=1)
|
||||
* - CONNECTION (type=3)
|
||||
* - USER_PARAM (type=4)
|
||||
* Линии:
|
||||
* - TECH (type=0): только CREATE_CHANNEL (hasLine), root = HEADER
|
||||
* - TEXT (type=1): линии каналов, root = HEADER (канал "0") или CREATE_CHANNEL (канал "X")
|
||||
* - CONNECTION (type=3): одна линия
|
||||
* - USER_PARAM (type=4): одна линия
|
||||
*
|
||||
* prevLineNumber по ТЗ — это GLOBAL blockNumber предыдущего блока линии.
|
||||
* thisLineNumber — внутренний номер линии (мы ведём локально: 1,2,3...)
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - Здесь НЕТ обращения к blockchain.LineIndex.
|
||||
* - Линии адресуются по msg_type (type).
|
||||
* Важно:
|
||||
* - prevLineNumber — это GLOBAL blockNumber предыдущего блока линии.
|
||||
* - thisLineNumber — внутренний номер линии (для постов: 0,1,2...; для тех-линии: 1,2,3...)
|
||||
*/
|
||||
public final class ChainState {
|
||||
|
||||
// какие msg_type имеют линейную цепочку по ТЗ
|
||||
public static final short TYPE_HEADER = 0;
|
||||
public static final short TYPE_TECH = 0; // header/create_channel
|
||||
public static final short TYPE_TEXT = 1;
|
||||
public static final short TYPE_REACTION = 2;
|
||||
public static final short TYPE_CONNECTION = 3;
|
||||
@ -42,13 +44,25 @@ public final class ChainState {
|
||||
// header (block#0)
|
||||
private byte[] headerHash32 = null;
|
||||
|
||||
/**
|
||||
* line state per TYPE (только для TEXT/CONNECTION/USER_PARAM):
|
||||
* - lastGlobalNumber: последний GLOBAL blockNumber в линии
|
||||
* - lastHashHex: hash последнего блока линии
|
||||
* - lastThisLineNumber: последний thisLineNumber (внутренний)
|
||||
*/
|
||||
private static final class LineState {
|
||||
private final Map<Integer, byte[]> hash32ByNumber = new HashMap<>();
|
||||
|
||||
// ---------- TECH line state ----------
|
||||
private static final class TechLineState {
|
||||
int lastGlobalNumber = -1; // последний TECH-блок (HEADER или CREATE_CHANNEL)
|
||||
String lastHashHex = "";
|
||||
int lastThisLineNumber = 0; // 0 у HEADER (логически), дальше 1,2,3...
|
||||
|
||||
void reset() {
|
||||
lastGlobalNumber = -1;
|
||||
lastHashHex = "";
|
||||
lastThisLineNumber = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private final TechLineState techLine = new TechLineState();
|
||||
|
||||
// ---------- CONNECTION/USER_PARAM line state ----------
|
||||
private static final class SimpleLineState {
|
||||
int lastGlobalNumber = -1;
|
||||
String lastHashHex = "";
|
||||
int lastThisLineNumber = 0;
|
||||
@ -60,14 +74,32 @@ public final class ChainState {
|
||||
}
|
||||
}
|
||||
|
||||
private final LineState textLine = new LineState();
|
||||
private final LineState connectionLine = new LineState();
|
||||
private final LineState userParamLine = new LineState();
|
||||
private final SimpleLineState connectionLine = new SimpleLineState();
|
||||
private final SimpleLineState userParamLine = new SimpleLineState();
|
||||
|
||||
private final Map<Integer, byte[]> hash32ByNumber = new HashMap<>();
|
||||
// ---------- TEXT channels ----------
|
||||
public static final class ChannelLineState {
|
||||
final int rootBlockNumber;
|
||||
final String rootHashHex;
|
||||
|
||||
int lastGlobalNumber;
|
||||
String lastHashHex;
|
||||
int lastThisLineNumber; // перед первым постом = -1, чтобы первый был 0
|
||||
|
||||
ChannelLineState(int rootBlockNumber, String rootHashHex) {
|
||||
this.rootBlockNumber = rootBlockNumber;
|
||||
this.rootHashHex = rootHashHex;
|
||||
this.lastGlobalNumber = rootBlockNumber;
|
||||
this.lastHashHex = rootHashHex;
|
||||
this.lastThisLineNumber = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// rootBlockNumber -> state
|
||||
private final Map<Integer, ChannelLineState> textChannels = new HashMap<>();
|
||||
|
||||
public ChainState() {
|
||||
textLine.reset();
|
||||
techLine.reset();
|
||||
connectionLine.reset();
|
||||
userParamLine.reset();
|
||||
}
|
||||
@ -113,38 +145,72 @@ public final class ChainState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Является ли type "линейным" по ТЗ (т.е. нужно вести prevLine/thisLine). */
|
||||
public static boolean isLineType(short type) {
|
||||
int t = type & 0xFFFF;
|
||||
return t == TYPE_TEXT || t == TYPE_CONNECTION || t == TYPE_USER_PARAM;
|
||||
}
|
||||
|
||||
/** Следующие line-поля для указанного TYPE (только TEXT/CONNECTION/USER_PARAM). */
|
||||
/** Следующие line-поля для TECH/CONNECTION/USER_PARAM. */
|
||||
public NextLine nextLineByType(short type) {
|
||||
if (!isLineType(type)) {
|
||||
throw new IllegalArgumentException("Type " + (type & 0xFFFF) + " не использует line-поля по ТЗ");
|
||||
}
|
||||
if (!hasHeader()) {
|
||||
throw new IllegalStateException("Нельзя формировать line-поля до HEADER (нет headerHash32)");
|
||||
}
|
||||
|
||||
LineState ls = lineStateByType(type);
|
||||
int t = type & 0xFFFF;
|
||||
|
||||
if (t == TYPE_TECH) {
|
||||
// tech-line: prev = последний TECH; первый CREATE_CHANNEL -> prev = HEADER
|
||||
if (techLine.lastGlobalNumber == -1) {
|
||||
// после HEADER мы должны инициализировать techLine (делаем в applyHeader)
|
||||
throw new IllegalStateException("TECH line is not initialized yet");
|
||||
}
|
||||
return new NextLine(techLine.lastGlobalNumber, hexToBytes32(techLine.lastHashHex), techLine.lastThisLineNumber + 1);
|
||||
}
|
||||
|
||||
if (t == TYPE_CONNECTION) {
|
||||
return nextSimpleLine(connectionLine);
|
||||
}
|
||||
if (t == TYPE_USER_PARAM) {
|
||||
return nextSimpleLine(userParamLine);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Type " + t + " не поддерживает nextLineByType()");
|
||||
}
|
||||
|
||||
private NextLine nextSimpleLine(SimpleLineState ls) {
|
||||
if (ls.lastGlobalNumber == -1) {
|
||||
// первый блок линии ссылается на HEADER (block#0)
|
||||
return new NextLine(0, headerHash32.clone(), 1);
|
||||
}
|
||||
|
||||
if (ls.lastHashHex == null || ls.lastHashHex.isBlank()) {
|
||||
throw new IllegalStateException("LineState.lastHashHex пуст, но lastGlobalNumber!=-1 (type=" + (type & 0xFFFF) + ")");
|
||||
throw new IllegalStateException("LineState.lastHashHex пуст, но lastGlobalNumber!=-1");
|
||||
}
|
||||
return new NextLine(ls.lastGlobalNumber, hexToBytes32(ls.lastHashHex), ls.lastThisLineNumber + 1);
|
||||
}
|
||||
|
||||
return new NextLine(ls.lastGlobalNumber, hexToBytes32(ls.lastHashHex), ls.lastThisLineNumber + 1);
|
||||
/** Следующие line-поля для TEXT-канала по rootBlockNumber. */
|
||||
public NextLine nextTextLineByRoot(int rootBlockNumber) {
|
||||
if (!hasHeader()) throw new IllegalStateException("No HEADER");
|
||||
ChannelLineState cs = textChannels.get(rootBlockNumber);
|
||||
if (cs == null) throw new IllegalStateException("Unknown TEXT channel rootBlockNumber=" + rootBlockNumber);
|
||||
|
||||
return new NextLine(
|
||||
cs.lastGlobalNumber,
|
||||
hexToBytes32(cs.lastHashHex),
|
||||
cs.lastThisLineNumber + 1
|
||||
);
|
||||
}
|
||||
|
||||
/** Зарегистрировать новый канал TEXT по root = CREATE_CHANNEL block. */
|
||||
public void registerTextChannelRoot(int rootBlockNumber, byte[] rootHash32) {
|
||||
if (rootBlockNumber <= 0) throw new IllegalArgumentException("rootBlockNumber must be > 0 for custom channel");
|
||||
if (rootHash32 == null || rootHash32.length != 32) throw new IllegalArgumentException("rootHash32 invalid");
|
||||
textChannels.put(rootBlockNumber, new ChannelLineState(rootBlockNumber, bytesToHex64(rootHash32)));
|
||||
}
|
||||
|
||||
/** root канала "0" (по умолчанию) — это HEADER block#0. */
|
||||
public int rootChannel0() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// -------------------- apply --------------------
|
||||
|
||||
public void applyAppendedBlock(int blockNumber, byte[] hash32, boolean isHeader, short type) {
|
||||
public void applyAppendedBlock(int blockNumber, byte[] hash32, boolean isHeader, short type, BodyRecord body) {
|
||||
if (hash32 == null || hash32.length != 32) {
|
||||
throw new IllegalArgumentException("hash32 must be 32 bytes");
|
||||
}
|
||||
@ -167,30 +233,64 @@ public final class ChainState {
|
||||
|
||||
hash32ByNumber.put(blockNumber, hash32.clone());
|
||||
|
||||
// обновляем line-state только если этот type по ТЗ линейный
|
||||
if (isLineType(type)) {
|
||||
LineState ls = lineStateByType(type);
|
||||
ls.lastGlobalNumber = blockNumber;
|
||||
ls.lastHashHex = hex64;
|
||||
// thisLineNumber обновляется отдельным вызовом (см. applyThisLineNumberByType)
|
||||
}
|
||||
// ---- init after HEADER ----
|
||||
if (isHeader) {
|
||||
// TECH line root = HEADER
|
||||
techLine.lastGlobalNumber = 0;
|
||||
techLine.lastHashHex = hex64;
|
||||
techLine.lastThisLineNumber = 0;
|
||||
|
||||
// TEXT channel "0" root = HEADER, первый пост будет thisLineNumber=0
|
||||
textChannels.put(0, new ChannelLineState(0, hex64));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** В тестах удобно явно обновлять thisLineNumber после успешной отправки line-body. */
|
||||
public void applyThisLineNumberByType(short type, int thisLineNumber) {
|
||||
if (!isLineType(type)) return;
|
||||
LineState ls = lineStateByType(type);
|
||||
ls.lastThisLineNumber = thisLineNumber;
|
||||
}
|
||||
|
||||
private LineState lineStateByType(short type) {
|
||||
int t = type & 0xFFFF;
|
||||
return switch (t) {
|
||||
case TYPE_TEXT -> textLine;
|
||||
case TYPE_CONNECTION -> connectionLine;
|
||||
case TYPE_USER_PARAM -> userParamLine;
|
||||
default -> throw new IllegalArgumentException("Type " + t + " не имеет LineState по ТЗ");
|
||||
};
|
||||
|
||||
// ---- TECH (CREATE_CHANNEL) ----
|
||||
if (t == TYPE_TECH && body instanceof CreateChannelBody ccb) {
|
||||
techLine.lastGlobalNumber = blockNumber;
|
||||
techLine.lastHashHex = hex64;
|
||||
techLine.lastThisLineNumber = ccb.thisLineNumber;
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- CONNECTION / USER_PARAM ----
|
||||
if (t == TYPE_CONNECTION && body instanceof BodyHasLine hlc) {
|
||||
connectionLine.lastGlobalNumber = blockNumber;
|
||||
connectionLine.lastHashHex = hex64;
|
||||
connectionLine.lastThisLineNumber = hlc.thisLineNumber();
|
||||
return;
|
||||
}
|
||||
if (t == TYPE_USER_PARAM && body instanceof BodyHasLine hlu) {
|
||||
userParamLine.lastGlobalNumber = blockNumber;
|
||||
userParamLine.lastHashHex = hex64;
|
||||
userParamLine.lastThisLineNumber = hlu.thisLineNumber();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- TEXT channels (POST/EDIT_POST) ----
|
||||
if (t == TYPE_TEXT && body instanceof TextBody tb) {
|
||||
if (tb.isLineMessage()) {
|
||||
// ищем канал по совпадению prevLineNumber с lastGlobalNumber канала
|
||||
ChannelLineState channel = findTextChannelByLastGlobal(tb.prevLineNumber);
|
||||
if (channel == null) {
|
||||
throw new IllegalStateException("TEXT line message prevLineNumber=" + tb.prevLineNumber + " не привязан ни к одному каналу (канал root не зарегистрирован?)");
|
||||
}
|
||||
|
||||
channel.lastGlobalNumber = blockNumber;
|
||||
channel.lastHashHex = hex64;
|
||||
channel.lastThisLineNumber = tb.thisLineNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelLineState findTextChannelByLastGlobal(int prevLineNumber) {
|
||||
for (ChannelLineState cs : textChannels.values()) {
|
||||
if (cs.lastGlobalNumber == prevLineNumber) return cs;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// -------------------- utils --------------------
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package test.it.cases;
|
||||
|
||||
import blockchain.body.*;
|
||||
import blockchain.MsgSubType;
|
||||
import blockchain.body.*;
|
||||
import test.it.blockchain.AddBlockSender;
|
||||
import test.it.blockchain.ChainState;
|
||||
import test.it.utils.TestConfig;
|
||||
@ -14,12 +14,12 @@ import java.time.Duration;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* IT_03_AddBlock_NoAuth — обновлён под новый формат блоков (ТЗ).
|
||||
* IT_03_AddBlock_NoAuth — сценарий блоков (новый формат + каналы).
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - НЕТ обращения к blockchain.LineIndex (можно удалить LineIndex.java).
|
||||
* - Линии берём через ChainState.nextLineByType(TYPE_...).
|
||||
* - ConnectionBody: toLogin в байтах НЕ хранится, вычисляется из toBlockchainName.
|
||||
* - TECH: Header + CreateChannel идут по тех-линии (hasLine у CreateChannel).
|
||||
* - TEXT: посты в каналах — отдельные линии, root = Header(канал "0") или CreateChannel(канал "X").
|
||||
* - REPLY (subType=20): без линии, target может указывать на чужой блокчейн, и ОБЯЗАТЕЛЬНО содержит toBlockNumber+toBlockHash32.
|
||||
*/
|
||||
public class IT_03_AddBlock_NoAuth {
|
||||
|
||||
@ -54,184 +54,123 @@ public class IT_03_AddBlock_NoAuth {
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// USER1
|
||||
// =========================
|
||||
ChainState st1 = new ChainState();
|
||||
AddBlockSender sender1 = new AddBlockSender(ws, st1, u1, bch1, TestConfig.getBlockchainPrivatKey(u1));
|
||||
|
||||
sender1.send(new HeaderBody(u1), t);
|
||||
assertTrue(st1.hasHeader());
|
||||
|
||||
// TEXT_NEW x3 (с line)
|
||||
// канал "0" (root=HEADER) — по умолчанию существует
|
||||
int root0 = st1.rootChannel0();
|
||||
|
||||
// POST в канал "0"
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_NEW,
|
||||
"Hello #1 (NEW) from IT_03 test",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_NEW,
|
||||
"Hello #2 (NEW) from IT_03 test",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_NEW,
|
||||
"Hello #3 (NEW) from IT_03 test",
|
||||
var ln = st1.nextTextLineByRoot(root0);
|
||||
sender1.send(new TextBody(
|
||||
MsgSubType.TEXT_POST,
|
||||
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"U1: story/post in channel 0",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
|
||||
byte[] text1Hash = st1.getHash32(1);
|
||||
byte[] text2Hash = st1.getHash32(2);
|
||||
byte[] text3Hash = st1.getHash32(3);
|
||||
assertNotNull(text1Hash);
|
||||
assertNotNull(text2Hash);
|
||||
assertNotNull(text3Hash);
|
||||
int post0Block = st1.lastBlockNumber();
|
||||
byte[] post0Hash = st1.getHash32(post0Block);
|
||||
assertNotNull(post0Hash);
|
||||
|
||||
// TEXT_REPLY x2 (с line + target)
|
||||
// CREATE_CHANNEL "News" (TECH line)
|
||||
int newsRootBlock;
|
||||
byte[] newsRootHash;
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_REPLY,
|
||||
"Reply to TEXT#1",
|
||||
bch1, 1, text1Hash
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TECH);
|
||||
sender1.send(new CreateChannelBody(
|
||||
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"News"
|
||||
), t);
|
||||
|
||||
newsRootBlock = st1.lastBlockNumber();
|
||||
newsRootHash = st1.getHash32(newsRootBlock);
|
||||
assertNotNull(newsRootHash);
|
||||
|
||||
// зарегистрируем root канала для тестового state, чтобы nextTextLineByRoot() работал
|
||||
st1.registerTextChannelRoot(newsRootBlock, newsRootHash);
|
||||
}
|
||||
|
||||
// POST #0 в канал "News"
|
||||
int newsPost0Block;
|
||||
byte[] newsPost0Hash;
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_REPLY,
|
||||
"Reply to TEXT#3",
|
||||
bch1, 3, text3Hash
|
||||
var ln = st1.nextTextLineByRoot(newsRootBlock);
|
||||
sender1.send(new TextBody(
|
||||
MsgSubType.TEXT_POST,
|
||||
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"U1: News post #0",
|
||||
null, null, null
|
||||
), t);
|
||||
|
||||
newsPost0Block = st1.lastBlockNumber();
|
||||
newsPost0Hash = st1.getHash32(newsPost0Block);
|
||||
assertNotNull(newsPost0Hash);
|
||||
}
|
||||
|
||||
// POST #1 в канал "News"
|
||||
{
|
||||
var ln = st1.nextTextLineByRoot(newsRootBlock);
|
||||
sender1.send(new TextBody(
|
||||
MsgSubType.TEXT_POST,
|
||||
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"U1: News post #1",
|
||||
null, null, null
|
||||
), t);
|
||||
}
|
||||
|
||||
// REACTION_LIKE x2 (без line)
|
||||
sender1.send(new ReactionBody(bch1, 1, text1Hash), t);
|
||||
sender1.send(new ReactionBody(bch1, 2, text2Hash), t);
|
||||
|
||||
// TEXT_EDIT x3 (с line + target)
|
||||
// EDIT_POST (не увеличивает thisLineNumber, но является частью линии)
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_EDIT,
|
||||
"Hello #2 (EDIT#1) from IT_03 test",
|
||||
bch1, 2, text2Hash
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_EDIT,
|
||||
"Hello #2 (EDIT#2) from IT_03 test",
|
||||
bch1, 2, text2Hash
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_TEXT);
|
||||
sender1.send(new TextBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.TEXT_EDIT,
|
||||
"Hello #3 (EDIT#1) from IT_03 test",
|
||||
bch1, 3, text3Hash
|
||||
var ln = st1.nextTextLineByRoot(newsRootBlock);
|
||||
// edit должен иметь thisLineNumber как у предыдущего сообщения линии (ChainState это уже даёт)
|
||||
sender1.send(new TextBody(
|
||||
MsgSubType.TEXT_EDIT_POST,
|
||||
ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"U1: News post #0 (EDIT)",
|
||||
null,
|
||||
newsPost0Block,
|
||||
newsPost0Hash
|
||||
), t);
|
||||
}
|
||||
|
||||
assertEquals(10, st1.lastBlockNumber(), "USER1: lastBlockNumber должен быть 10 (всего 11 блоков включая HEADER)");
|
||||
|
||||
// USER2
|
||||
// =========================
|
||||
// USER2 (ответ в чужой канал)
|
||||
// =========================
|
||||
ChainState st2 = new ChainState();
|
||||
AddBlockSender sender2 = new AddBlockSender(ws, st2, u2, bch2, TestConfig.getBlockchainPrivatKey(u2));
|
||||
|
||||
sender2.send(new HeaderBody(u2), t);
|
||||
assertTrue(st2.hasHeader());
|
||||
|
||||
// USER_PARAM (с line)
|
||||
// REPLY (20): ответ на post в чужом блокчейне/канале
|
||||
{
|
||||
var ln = st2.nextLineByType(ChainState.TYPE_USER_PARAM);
|
||||
sender2.send(new UserParamBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"Anya", "Amsterdam, Example street 10"
|
||||
sender2.send(new TextBody(
|
||||
MsgSubType.TEXT_REPLY,
|
||||
-1, new byte[32], -1, // для replies линии нет
|
||||
"U2: reply to U1 News post #0 (cross-chain)",
|
||||
bch1,
|
||||
newsPost0Block,
|
||||
newsPost0Hash
|
||||
), t);
|
||||
}
|
||||
|
||||
// USER3 (нужен, чтобы u1 мог подписаться на существующий блокчейн)
|
||||
// =========================
|
||||
// USER3 (просто чтобы оставалось как раньше)
|
||||
// =========================
|
||||
ChainState st3 = new ChainState();
|
||||
AddBlockSender sender3 = new AddBlockSender(ws, st3, u3, bch3, TestConfig.getBlockchainPrivatKey(u3));
|
||||
|
||||
sender3.send(new HeaderBody(u3), t);
|
||||
assertTrue(st3.hasHeader());
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Подписки:
|
||||
// - u1 follows u2 и u3
|
||||
// - u2 follows только u1
|
||||
// Все CONNECTION идут по линии CONNECTION (по ТЗ "да надо")
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// u1 -> follow u2
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender1.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FOLLOW,
|
||||
bch2, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// u1 -> follow u3
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender1.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FOLLOW,
|
||||
bch3, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// u2 -> follow u1
|
||||
{
|
||||
var ln = st2.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender2.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FOLLOW,
|
||||
bch1, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// friend/unfriend как было, но тоже по CONNECTION линии
|
||||
{
|
||||
var ln = st2.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender2.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FRIEND,
|
||||
bch1, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
// user1 param + friend to u2
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_USER_PARAM);
|
||||
sender1.send(new UserParamBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
"Anna", "Gareeva"
|
||||
), t);
|
||||
}
|
||||
{
|
||||
var ln = st1.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender1.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_FRIEND,
|
||||
bch2, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
{
|
||||
var ln = st2.nextLineByType(ChainState.TYPE_CONNECTION);
|
||||
sender2.send(new ConnectionBody(ln.prevLineNumber, ln.prevLineHash32, ln.thisLineNumber,
|
||||
MsgSubType.CONNECTION_UNFRIEND,
|
||||
bch1, 0, new byte[32]
|
||||
), t);
|
||||
}
|
||||
|
||||
r.ok("IT_03 сценарий блоков выполнен");
|
||||
|
||||
} catch (Throwable e) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user