From 272d7ca1be31ae4e68e4644d5658c6231cacfd85d1ec200e6c00d5bf4f6d341f Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 2 Jan 2026 18:16:59 +0300 Subject: [PATCH] =?UTF-8?q?02=2001=2025=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B1=D0=BE=D0=B4=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=81=D0=B2=D1=8F=D0=B7=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Дальше делать: Описание форматов. Запросы клиент-сервер. Промт на клиента. --- Потом в сервак дописать Синхронизацию серверов. --- shine-server-blockchain/concat_to_file.sh | 6 +- .../blockchain/body/BodyRecordParser.java | 8 +- .../java/blockchain/body/ConnectionBody.java | 280 ++++++++++++++++++ .../main/java/blockchain/body/HeaderBody.java | 2 + .../java/blockchain/body/ReactionBody.java | 2 + .../main/java/blockchain/body/TextBody.java | 2 + 6 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java diff --git a/shine-server-blockchain/concat_to_file.sh b/shine-server-blockchain/concat_to_file.sh index 901712c..f6db1f1 100755 --- a/shine-server-blockchain/concat_to_file.sh +++ b/shine-server-blockchain/concat_to_file.sh @@ -12,5 +12,9 @@ find . -type f -name "*.java" | sort | while read -r f; do echo >> "$OUTFILE" # пустая строка-разделитель done -echo "Готово! Все .java файлы собраны в $OUTFILE" +# скопировать весь файл в буфер обмена (Wayland) +wl-copy < "$OUTFILE" +echo "Готово!" +echo "Все .java файлы собраны в $OUTFILE" +echo "Содержимое скопировано в буфер обмена (Wayland)" diff --git a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java index 410a68a..a492f29 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/BodyRecordParser.java @@ -18,10 +18,10 @@ public final class BodyRecordParser { int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF); return switch (key) { - case 0x0000_0001 -> new HeaderBody(bodyBytes); // type=0, ver=1 // заглавие блокчейна - case 0x0001_0001 -> new TextBody(bodyBytes); // type=1, ver=1 // текстовое сообщение - case 0x0002_0001 -> new ReactionBody(bodyBytes); // type=2, ver=1 // реакция - case 0x0003_0001 -> new LinkBody(bodyBytes); // type=3, ver=1 // связь + case HeaderBody.KEY -> new HeaderBody(bodyBytes); // type=0, ver=1 + case TextBody.KEY -> new TextBody(bodyBytes); // type=1, ver=1 + case ReactionBody.KEY -> new ReactionBody(bodyBytes); // type=2, ver=1 + case ConnectionBody.KEY -> new ConnectionBody(bodyBytes); // type=3, ver=1 default -> throw new IllegalArgumentException(String.format( "Unknown body type/version: type=%d ver=%d (key=0x%08X)", (type & 0xFFFF), (ver & 0xFFFF), key diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java new file mode 100644 index 0000000..6e7932f --- /dev/null +++ b/shine-server-blockchain/src/main/java/blockchain/body/ConnectionBody.java @@ -0,0 +1,280 @@ +package blockchain.body; + +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 3; + } + + @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); + } +} \ No newline at end of file diff --git a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java index 6ac7711..973056a 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/HeaderBody.java @@ -28,6 +28,8 @@ public final class HeaderBody implements BodyRecord { public static final short TYPE = 0; public static final short VER = 1; + public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF); + /** Для header всегда 0 (служебная совместимость). */ public static final short SUBTYPE_COMPAT = 0; diff --git a/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java b/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java index abf6fc9..6698ad8 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/ReactionBody.java @@ -34,6 +34,8 @@ public final class ReactionBody implements BodyRecord { public static final short TYPE = 2; public static final short VER = 1; + public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF); + // subType: public static final short SUB_LIKE = 1; diff --git a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java index 30cb56e..7a7891d 100644 --- a/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java +++ b/shine-server-blockchain/src/main/java/blockchain/body/TextBody.java @@ -44,6 +44,8 @@ public final class TextBody implements BodyRecord { 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;