SHA256
Доделал тесты и названия линий сделал в константы Дальше делать: Описание форматов. Запросы клиент-сервер. Промт на клиента. --- Потом в сервак дописать Синхронизацию серверов.
282 lines
11 KiB
Java
282 lines
11 KiB
Java
package blockchain.body;
|
||
|
||
import blockchain.LineIndex;
|
||
|
||
import java.nio.ByteBuffer;
|
||
import java.nio.ByteOrder;
|
||
import java.nio.charset.StandardCharsets;
|
||
import java.util.Arrays;
|
||
import java.util.Objects;
|
||
|
||
/**
|
||
* ConnectionBody — type=3, ver=1. (Связь/отношение)
|
||
*
|
||
* Идея:
|
||
* - Это запись "у меня есть связь с X".
|
||
* - subType определяет вид связи:
|
||
* 10 = FRIEND (друг)
|
||
* 20 = CONTACT (контакт)
|
||
* 30 = FOLLOW (подписан на кого-то)
|
||
*
|
||
* Формат bodyBytes (BigEndian):
|
||
* [2] type=3
|
||
* [2] ver=1
|
||
*
|
||
* [2] subType (uint16) — вид связи (10/20/30)
|
||
*
|
||
* [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 {
|
||
|
||
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:
|
||
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;
|
||
|
||
/** С кем связь (главное поле). */
|
||
public final String toLogin;
|
||
|
||
/** Блокчейн того человека (снимок/якорь). */
|
||
public final String toBlockchainName;
|
||
|
||
/** Номер последнего известного блока у того человека (снимок/якорь). */
|
||
public final int toBlockGlobalNumber;
|
||
|
||
/** Хэш последнего известного блока у того человека (снимок/якорь). */
|
||
public final byte[] toBlockHash32;
|
||
|
||
/* ===================================================================== */
|
||
/* ====================== Конструктор из байт =========================== */
|
||
/* ===================================================================== */
|
||
|
||
public ConnectionBody(byte[] bodyBytes) {
|
||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||
|
||
// минимум:
|
||
// 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) {
|
||
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.subType = bb.getShort();
|
||
if (!isValidSubType(this.subType)) {
|
||
throw new IllegalArgumentException("Bad connection subType: " + (this.subType & 0xFFFF));
|
||
}
|
||
|
||
// --- 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");
|
||
|
||
byte[] toLoginBytes = new byte[toLoginLen];
|
||
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");
|
||
|
||
byte[] bchBytes = new byte[bchLen];
|
||
bb.get(bchBytes);
|
||
this.toBlockchainName = new String(bchBytes, StandardCharsets.UTF_8);
|
||
|
||
this.toBlockGlobalNumber = bb.getInt();
|
||
|
||
this.toBlockHash32 = new byte[32];
|
||
bb.get(this.toBlockHash32);
|
||
|
||
// запрет мусора в конце
|
||
if (bb.remaining() != 0) {
|
||
throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
||
}
|
||
}
|
||
|
||
/* ===================================================================== */
|
||
/* ====================== Конструктор “вручную” ========================= */
|
||
/* ===================================================================== */
|
||
|
||
public ConnectionBody(short subType,
|
||
String toLogin,
|
||
String toBlockchainName,
|
||
int toBlockGlobalNumber,
|
||
byte[] toBlockHash32) {
|
||
|
||
Objects.requireNonNull(toLogin, "toLogin == null");
|
||
Objects.requireNonNull(toBlockchainName, "toBlockchainName == null");
|
||
Objects.requireNonNull(toBlockHash32, "toBlockHash32 == null");
|
||
|
||
if (!isValidSubType(subType)) {
|
||
throw new IllegalArgumentException("Unknown 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 (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.toLogin = toLogin;
|
||
this.toBlockchainName = toBlockchainName;
|
||
this.toBlockGlobalNumber = toBlockGlobalNumber;
|
||
this.toBlockHash32 = Arrays.copyOf(toBlockHash32, 32);
|
||
}
|
||
|
||
private static boolean isValidSubType(short st) {
|
||
return st == SUB_FRIEND || st == SUB_CONTACT || st == SUB_FOLLOW;
|
||
}
|
||
|
||
/* ===================================================================== */
|
||
/* ====================== 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;
|
||
}
|
||
|
||
@Override
|
||
public ConnectionBody check() {
|
||
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_]+$");
|
||
|
||
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;
|
||
}
|
||
|
||
@Override
|
||
public byte[] toBytes() {
|
||
byte[] toLoginBytes = toLogin.getBytes(StandardCharsets.UTF_8);
|
||
if (toLoginBytes.length == 0 || toLoginBytes.length > 255)
|
||
throw new IllegalArgumentException("toLogin utf8 len must be 1..255");
|
||
|
||
byte[] bchBytes = toBlockchainName.getBytes(StandardCharsets.UTF_8);
|
||
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
|
||
+ 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.put((byte) toLoginBytes.length);
|
||
bb.put(toLoginBytes);
|
||
|
||
bb.put((byte) bchBytes.length);
|
||
bb.put(bchBytes);
|
||
|
||
bb.putInt(toBlockGlobalNumber);
|
||
bb.put(toBlockHash32);
|
||
|
||
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)";
|
||
default -> "UNKNOWN";
|
||
};
|
||
|
||
return """
|
||
ConnectionBody {
|
||
тип записи : CONNECTION (type=3, ver=1)
|
||
ожидаемая линия : 3
|
||
subType : %s
|
||
связь с login : "%s"
|
||
блокчейн друга/цели : "%s"
|
||
lastKnown globalNumber : %d
|
||
lastKnown hash (hex) : %s
|
||
}
|
||
""".formatted(
|
||
st,
|
||
toLogin,
|
||
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);
|
||
}
|
||
} |