Browse Source

chatgui: invite link dialogs, zxing-cpp QR code, crypto transport, channel improvements; msg_transport: chat protocol support

feature/x25519-migration
Evgeny 3 months ago
parent
commit
22c24d135e
  1. 11
      src/chat/chat_tz.txt
  2. 53
      src/msg_transport.c
  3. 8
      src/msg_transport.h
  4. 47
      tools/chatgui/CMakeLists.txt
  5. 7
      tools/chatgui/Makefile
  6. 214
      tools/chatgui/chat_tz.txt
  7. 167
      tools/chatgui/chat_tz.txt1
  8. 635
      tools/chatgui/db/db_manager.cpp
  9. 67
      tools/chatgui/db/db_manager.h
  10. 19
      tools/chatgui/doc/db_schema.md
  11. 28
      tools/chatgui/src/accountlist.cpp
  12. 10
      tools/chatgui/src/accountlist.h
  13. 7
      tools/chatgui/src/channeldelegate.h
  14. 69
      tools/chatgui/src/channellist.cpp
  15. 18
      tools/chatgui/src/channellist.h
  16. 15
      tools/chatgui/src/inputbar.cpp
  17. 100
      tools/chatgui/src/invite_link.cpp
  18. 26
      tools/chatgui/src/invite_link.h
  19. 47
      tools/chatgui/src/invitedialog.cpp
  20. 13
      tools/chatgui/src/invitedialog.h
  21. 22
      tools/chatgui/src/main.cpp
  22. 67
      tools/chatgui/src/mainwindow.cpp
  23. 15
      tools/chatgui/src/mainwindow.h
  24. 187
      tools/chatgui/src/messagelist.cpp
  25. 23
      tools/chatgui/src/messagelist.h
  26. 26
      tools/chatgui/src/qrcode_utils.cpp
  27. 9
      tools/chatgui/src/qrcode_utils.h
  28. 247
      tools/chatgui/tests/test_crypto.cpp
  29. 376
      tools/chatgui/transport/chat_propagator.cpp
  30. 70
      tools/chatgui/transport/chat_propagator.h
  31. 48
      tools/chatgui/transport/chat_proto.h
  32. 221
      tools/chatgui/transport/crypto.cpp
  33. 48
      tools/chatgui/transport/crypto.h
  34. 31
      tools/chatgui/transport/msg_client.cpp
  35. 15
      tools/chatgui/transport/msg_client.h
  36. 43
      tools/chatgui/zxing-cpp/.clang-format
  37. 58
      tools/chatgui/zxing-cpp/.clang-tidy
  38. 19
      tools/chatgui/zxing-cpp/.editorconfig
  39. 3
      tools/chatgui/zxing-cpp/.gitattributes
  40. 2
      tools/chatgui/zxing-cpp/.github/FUNDING.yml
  41. 102
      tools/chatgui/zxing-cpp/.github/create_release_archive.py
  42. 435
      tools/chatgui/zxing-cpp/.github/workflows/ci.yml
  43. 59
      tools/chatgui/zxing-cpp/.github/workflows/codeql-analysis.yml
  44. 71
      tools/chatgui/zxing-cpp/.github/workflows/msvc-analysis.yml
  45. 41
      tools/chatgui/zxing-cpp/.github/workflows/publish-android.yml
  46. 130
      tools/chatgui/zxing-cpp/.github/workflows/publish-dotnet.yml
  47. 131
      tools/chatgui/zxing-cpp/.github/workflows/publish-gh-pages.yml
  48. 97
      tools/chatgui/zxing-cpp/.github/workflows/publish-kn.yml
  49. 100
      tools/chatgui/zxing-cpp/.github/workflows/publish-python.yml
  50. 39
      tools/chatgui/zxing-cpp/.github/workflows/publish-source.yml
  51. 82
      tools/chatgui/zxing-cpp/.github/workflows/publish-winrt.yml
  52. 27
      tools/chatgui/zxing-cpp/.gitignore
  53. 4
      tools/chatgui/zxing-cpp/.gitmodules
  54. 121
      tools/chatgui/zxing-cpp/CMakeLists.txt
  55. 202
      tools/chatgui/zxing-cpp/LICENSE
  56. 38
      tools/chatgui/zxing-cpp/Package.swift
  57. 154
      tools/chatgui/zxing-cpp/README.md
  58. 29
      tools/chatgui/zxing-cpp/docs/CMakeLists.txt
  59. 1373
      tools/chatgui/zxing-cpp/docs/Doxyfile.in
  60. 58
      tools/chatgui/zxing-cpp/example/CMakeLists.txt
  61. 106
      tools/chatgui/zxing-cpp/example/ZXingBarcodeFormat.cpp
  62. 31
      tools/chatgui/zxing-cpp/example/ZXingOpenCV.cpp
  63. 45
      tools/chatgui/zxing-cpp/example/ZXingOpenCV.h
  64. 392
      tools/chatgui/zxing-cpp/example/ZXingReader.cpp
  65. 235
      tools/chatgui/zxing-cpp/example/ZXingWriter.cpp
  66. 752
      tools/chatgui/zxing-cpp/test/blackbox/BlackboxTestRunner.cpp
  67. 18
      tools/chatgui/zxing-cpp/test/blackbox/BlackboxTestRunner.h
  68. 37
      tools/chatgui/zxing-cpp/test/blackbox/CMakeLists.txt
  69. 105
      tools/chatgui/zxing-cpp/test/blackbox/ImageLoader.cpp
  70. 18
      tools/chatgui/zxing-cpp/test/blackbox/ImageLoader.h
  71. 65
      tools/chatgui/zxing-cpp/test/blackbox/TestReaderMain.cpp
  72. 48
      tools/chatgui/zxing-cpp/test/blackbox/TestWriterMain.cpp
  73. 34
      tools/chatgui/zxing-cpp/test/fuzz/CMakeLists.txt
  74. 31
      tools/chatgui/zxing-cpp/test/fuzz/fuzzDBEDecoder.cpp
  75. 24
      tools/chatgui/zxing-cpp/test/fuzz/fuzzDMEncoder.cpp
  76. 87
      tools/chatgui/zxing-cpp/test/fuzz/fuzzDecodeMatrix.cpp
  77. 68
      tools/chatgui/zxing-cpp/test/fuzz/fuzzReadLinear.cpp
  78. 64
      tools/chatgui/zxing-cpp/test/fuzz/fuzzReadMatrix.cpp
  79. 17
      tools/chatgui/zxing-cpp/test/install/CMakeLists.txt
  80. 22
      tools/chatgui/zxing-cpp/test/install/main.cpp
  81. 20
      tools/chatgui/zxing-cpp/test/install/mainOLD.cpp
  82. 34
      tools/chatgui/zxing-cpp/test/install/run.cmake
  83. 1
      tools/chatgui/zxing-cpp/test/samples/.gitattributes
  84. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/#128.txt
  85. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/#128.webp
  86. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/#217.png
  87. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/#217.txt
  88. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/7.png
  89. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/7.result.txt
  90. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/7.txt
  91. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/BinarizerFallback.png
  92. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/BinarizerFallback.txt
  93. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/Historico.png
  94. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/Historico.txt
  95. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/HistoricoLong.png
  96. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/HistoricoLong.txt
  97. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/Z1-sequence4of7.png
  98. 4
      tools/chatgui/zxing-cpp/test/samples/aztec-1/Z1-sequence4of7.result.txt
  99. 1
      tools/chatgui/zxing-cpp/test/samples/aztec-1/Z1-sequence4of7.txt
  100. BIN
      tools/chatgui/zxing-cpp/test/samples/aztec-1/abc-19x19C.png
  101. Some files were not shown because too many files have changed in this diff Show More

11
src/chat/chat_tz.txt

@ -1,11 +0,0 @@
Архитектура чата (группового)
1. сообщения чата (текст + ссылки на аттачи) хранятся в таблице:
id (primary key), timestamp, user_id, msg (utf8 text 4k max), sign
2. реакции: key(msg_id, user_id), type, sign
3. аттачи: key(msg_id, user_id), filename, sign
Структура каталогов:
chats.db - база
<caht_id>

53
src/msg_transport.c

@ -9,6 +9,8 @@
#include "utun_instance.h"
#include "etcp_router.h"
#include "etcp.h"
#include "secure_channel.h"
#include "route_bgp.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
@ -44,6 +46,7 @@ struct msg_client {
uint8_t recv_buffer[MSG_MAX_MSG_SIZE];
uint16_t recv_len;
uint64_t bound_peer_id; /* 0 = не забинден */
uint8_t is_localhost; /* 1 = подключение с loopback */
};
struct msg_transport {
@ -275,6 +278,18 @@ static void accept_callback(socket_t fd, void* arg) {
client->fd = client_fd;
client->transport = t;
/* Определяем loopback */
if (client_addr.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&client_addr;
client->is_localhost = (ntohl(sin->sin_addr.s_addr) == 0x7F000001u); /* 127.0.0.1 */
} else if (client_addr.ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&client_addr;
static const uint8_t loopback_v6[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
client->is_localhost = (memcmp(&sin6->sin6_addr, loopback_v6, 16) == 0);
} else {
client->is_localhost = 0;
}
client->socket_id = uasync_add_socket_t(t->ua, client_fd,
client_read_callback, NULL,
client_except_callback, client);
@ -425,6 +440,44 @@ static void handle_client_data(struct msg_transport* t, struct msg_client* clien
DEBUG_INFO(DEBUG_CATEGORY_MSGTRANSPORT, "UNBIND");
break;
case MSG_CTL_GET_PUBKEY:
send_response(client, MSG_RSP_PUBKEY, 0,
t->instance->my_keys.public_key, SC_PUBKEY_SIZE);
break;
case MSG_CTL_GET_PRIVKEY:
if (!client->is_localhost) {
send_error(client, MSG_ERR_ACCESS_DENIED, "private key only available from localhost");
DEBUG_WARN(DEBUG_CATEGORY_MSGTRANSPORT, "GET_PRIVKEY denied: not localhost");
break;
}
send_response(client, MSG_RSP_PRIVKEY, 0,
t->instance->my_keys.private_key, SC_PRIVKEY_SIZE);
break;
case MSG_CTL_GET_PEER_INFO: {
uint64_t peer_id = hdr->id;
struct ROUTE_BGP* bgp = t->instance->bgp;
if (!bgp) {
send_error(client, MSG_ERR_PEER_NOT_FOUND, "BGP not initialized");
break;
}
struct NODEINFO_Q* nq = route_bgp_get_node(bgp, peer_id);
if (!nq) {
send_error(client, MSG_ERR_PEER_NOT_FOUND, "peer not found");
DEBUG_WARN(DEBUG_CATEGORY_MSGTRANSPORT, "GET_PEER_INFO: peer=%016llx not found",
(unsigned long long)peer_id);
break;
}
uint8_t buf[SC_PUBKEY_SIZE * 2];
memcpy(buf, nq->node.public_key, SC_PUBKEY_SIZE);
memcpy(buf + SC_PUBKEY_SIZE, nq->node.ed25519_public_key, SC_PUBKEY_SIZE);
send_response(client, MSG_RSP_PEER_INFO, peer_id, buf, SC_PUBKEY_SIZE * 2);
DEBUG_INFO(DEBUG_CATEGORY_MSGTRANSPORT, "GET_PEER_INFO: peer=%016llx sent pubkey+edpub",
(unsigned long long)peer_id);
break;
}
case MSG_DATA_SEND: {
uint64_t dst = hdr->id ? hdr->id : client->bound_peer_id;
if (dst == 0) {

8
src/msg_transport.h

@ -34,9 +34,15 @@ extern "C" {
/* Типы сообщений */
#define MSG_CTL_BIND 0x01 /* GUI→utun: привязать TCP-соединение к пиру id */
#define MSG_CTL_UNBIND 0x02 /* GUI→utun: отвязать */
#define MSG_CTL_GET_PUBKEY 0x03 /* GUI→utun: запросить публичный ключ */
#define MSG_CTL_GET_PRIVKEY 0x04 /* GUI→utun: запросить приватный ключ (только localhost) */
#define MSG_CTL_GET_PEER_INFO 0x05 /* GUI→utun: запросить ключи пира id (pubkey + ed25519_pubkey) */
#define MSG_DATA_SEND 0x10 /* GUI→utun: отправить сообщение пиру id */
#define MSG_DATA_RECV 0x11 /* utun→GUI: получено сообщение от пира id */
#define MSG_RSP_BIND_OK 0xE1 /* utun→GUI: BIND успешен */
#define MSG_RSP_PUBKEY 0xE2 /* utun→GUI: публичный ключ (32 байта) */
#define MSG_RSP_PRIVKEY 0xE3 /* utun→GUI: приватный ключ (32 байта) */
#define MSG_RSP_PEER_INFO 0xE4 /* utun→GUI: ключи пира (pubkey+edpub=64 байта) */
#define MSG_RSP_ERROR 0xFF /* utun→GUI: ошибка */
/* Коды ошибок для MSG_RSP_ERROR */
@ -44,6 +50,8 @@ extern "C" {
#define MSG_ERR_NOT_BOUND 0x02
#define MSG_ERR_NO_ROUTE 0x03
#define MSG_ERR_SEND_FAILED 0x04
#define MSG_ERR_ACCESS_DENIED 0x07
#define MSG_ERR_PEER_NOT_FOUND 0x08
#define MSG_ERR_ID_TAKEN 0x06
#pragma pack(push, 1)

47
tools/chatgui/CMakeLists.txt

@ -1,21 +1,41 @@
cmake_minimum_required(VERSION 3.16)
project(chatgui VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
find_package(Qt5 COMPONENTS Widgets Network QUIET)
find_package(Qt5 COMPONENTS Widgets Network Test QUIET)
if(NOT Qt5_FOUND)
find_package(Qt6 COMPONENTS Widgets Network REQUIRED)
find_package(Qt6 COMPONENTS Widgets Network Test REQUIRED)
set(QT_LIBS Qt6::Widgets Qt6::Network)
set(QT_TEST_LIB Qt6::Test)
else()
set(QT_LIBS Qt5::Widgets Qt5::Network)
set(QT_TEST_LIB Qt5::Test)
endif()
find_package(rlottie REQUIRED)
find_package(ZLIB REQUIRED)
find_package(OpenSSL REQUIRED)
set(ZXING_READERS ON CACHE BOOL "")
set(ZXING_WRITERS "OLD" CACHE STRING "")
set(ZXING_ENABLE_QRCODE ON CACHE BOOL "")
set(ZXING_ENABLE_AZTEC OFF CACHE BOOL "")
set(ZXING_ENABLE_DATAMATRIX OFF CACHE BOOL "")
set(ZXING_ENABLE_MAXICODE OFF CACHE BOOL "")
set(ZXING_ENABLE_PDF417 OFF CACHE BOOL "")
set(ZXING_ENABLE_1D OFF CACHE BOOL "")
set(ZXING_C_API OFF CACHE BOOL "")
set(ZXING_EXAMPLES OFF CACHE BOOL "")
set(ZXING_EXAMPLES_QT OFF CACHE BOOL "")
set(ZXING_BLACKBOX_TESTS OFF CACHE BOOL "")
set(ZXING_UNIT_TESTS OFF CACHE BOOL "")
set(BUILD_SHARED_LIBS OFF CACHE BOOL "")
add_subdirectory(zxing-cpp)
set_target_properties(ZXing PROPERTIES AUTOMOC OFF)
add_executable(chatgui
src/main.cpp
@ -32,7 +52,12 @@ add_executable(chatgui
src/inputbar.cpp
src/lottieicon.cpp
src/animtimer.cpp
src/qrcode_utils.cpp
src/invite_link.cpp
src/invitedialog.cpp
transport/msg_client.cpp
transport/crypto.cpp
transport/chat_propagator.cpp
db/db_manager.cpp
db/sqlite3.c
resources/chatgui.qrc
@ -41,4 +66,18 @@ add_executable(chatgui
target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/db)
target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1)
set_source_files_properties(db/sqlite3.c PROPERTIES LANGUAGE C)
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB pthread dl)
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing pthread dl)
# ============================================================================
# Tests
# ============================================================================
enable_testing()
add_executable(test_crypto
tests/test_crypto.cpp
transport/crypto.cpp
)
target_include_directories(test_crypto PRIVATE ${CMAKE_SOURCE_DIR})
target_link_libraries(test_crypto PRIVATE Qt5::Core ${QT_TEST_LIB} OpenSSL::Crypto pthread)
add_test(NAME test_crypto COMMAND test_crypto)

7
tools/chatgui/Makefile

@ -4,7 +4,12 @@ all:
@[ -f $(BUILDDIR)/Makefile ] || cmake -S . -B $(BUILDDIR)
cmake --build $(BUILDDIR)
check:
@[ -f $(BUILDDIR)/Makefile ] || cmake -S . -B $(BUILDDIR)
cmake --build $(BUILDDIR) --target test_crypto
cd $(BUILDDIR) && ctest --output-on-failure
clean:
rm -rf $(BUILDDIR)
.PHONY: all clean
.PHONY: all check clean

214
tools/chatgui/chat_tz.txt

@ -1,13 +1,201 @@
Архитектура чата (группового)
!! пока мысли - не использовать это как готовое тз
1. сообщения чата (текст + ссылки на аттачи) хранятся в таблице:
id (primary key), timestamp, user_id, msg (utf8 text 4k max), sign
2. реакции: key(msg_id, user_id), type, sign
3. аттачи: key(msg_id, user_id), filename, sign
Структура каталогов:
chats.db - база
<caht_id>
Архитектура чата (группового)
База данных: SQLite3 (WAL mode), chats.db
================================================================================
Схема БД (10 таблиц)
================================================================================
-- 1. local_identity — текущий локальный узел (одна строка, приватный ключ здесь)
-- приватный ключ получаем от utun через MSG_RSP_PRIVKEY (только localhost)
CREATE TABLE local_identity (
id INTEGER PRIMARY KEY CHECK (id = 1),
node_id INTEGER NOT NULL UNIQUE,
name TEXT NOT NULL,
x25519_pubkey BLOB NOT NULL, -- 32 bytes
x25519_privkey BLOB, -- 32 bytes (только на этом устройстве)
ed25519_pubkey BLOB, -- 32 bytes (derive из x25519 privkey)
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch())
);
-- 2. my_nodes — другие наши ноды (другие устройства, нет приватного ключа)
CREATE TABLE my_nodes (
node_id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
x25519_pubkey BLOB NOT NULL, -- 32 bytes
ed25519_pubkey BLOB, -- 32 bytes
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch())
);
-- 3. nodes — известные удалённые узлы (получаем через msg_transport от utun)
CREATE TABLE nodes (
node_id INTEGER PRIMARY KEY,
name TEXT,
x25519_pubkey BLOB NOT NULL, -- 32 bytes
ed25519_pubkey BLOB, -- 32 bytes (для проверки подписи пира)
is_online INTEGER DEFAULT 0,
last_seen_at INTEGER DEFAULT (unixepoch()),
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch())
);
-- 3a. accounts — UI-информация об узлах для отображения сообщений
CREATE TABLE accounts (
node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id),
display_name TEXT NOT NULL, -- "Alice"
avatar_color TEXT DEFAULT '#4A90E2', -- hex RGB
avatar_letter TEXT NOT NULL, -- "A"
is_contact INTEGER DEFAULT 1, -- показывать в боковой панели
created_at INTEGER DEFAULT (unixepoch())
);
-- 4. channels — каналы (DM и групповые)
CREATE TABLE channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL UNIQUE, -- "dm:nodeA_nodeB" (A<B) или "group:hash"
name TEXT NOT NULL,
owner_node_id INTEGER, -- создатель (NULL для DM)
is_dm INTEGER DEFAULT 0,
last_message TEXT,
last_msg_at INTEGER, -- unix timestamp ms
created_at INTEGER DEFAULT (unixepoch())
);
CREATE UNIQUE INDEX idx_channel_cid ON channels(channel_id);
-- 5. channel_members — участники канала
CREATE TABLE channel_members (
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
node_id INTEGER NOT NULL,
joined_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (channel_id, node_id)
);
CREATE INDEX idx_cm_node ON channel_members(node_id);
-- 6. messages — сообщения чата
-- signature = Ed25519 отправителя над (channel_id, author_node_id, content, timestamp)
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
author_node_id INTEGER NOT NULL,
content TEXT NOT NULL CHECK(length(content) <= 4096),
content_type TEXT DEFAULT 'text/plain',
timestamp INTEGER NOT NULL, -- unix milliseconds
signature BLOB NOT NULL CHECK(length(signature) == 64),
is_outgoing INTEGER DEFAULT 0,
is_read INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_msg_channel_time ON messages(channel_id, timestamp);
CREATE INDEX idx_msg_author ON messages(author_node_id);
-- дедупликация: один автор не может отправить два сообщения в один ms в один канал
CREATE UNIQUE INDEX idx_msg_dedup ON messages(channel_id, author_node_id, timestamp);
-- 7. reactions — реакции на сообщения
-- signature = Ed25519 отправителя над (message_id, node_id, emoji)
CREATE TABLE reactions (
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
node_id INTEGER NOT NULL,
emoji TEXT NOT NULL, -- unicode codepoint или :shortcode:
signature BLOB NOT NULL CHECK(length(signature) == 64),
created_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (message_id, node_id, emoji)
);
-- 8. attachments — вложения к сообщениям
-- signature = Ed25519 отправителя над (message_id, filename, content_type, size, sha256)
CREATE TABLE attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL, -- MIME type
size INTEGER NOT NULL, -- bytes
sha256 BLOB NOT NULL, -- 32 bytes
file_path TEXT, -- локальный путь к сохранённому файлу
signature BLOB NOT NULL CHECK(length(signature) == 64),
is_upload INTEGER DEFAULT 0, -- 0=скачано, 1=наше
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_att_msg ON attachments(message_id);
-- 9. ui_state — сохранение состояния UI (последний канал, позиция скролла и т.д.)
CREATE TABLE ui_state (
key TEXT PRIMARY KEY,
value TEXT
);================================================================================
Основные запросы
================================================================================
-- Текущая нода (приватный ключ — только на этом устройстве)
SELECT node_id, name, x25519_pubkey, x25519_privkey, ed25519_pubkey
FROM local_identity WHERE id = 1;
-- Другие наши ноды (без приватных ключей)
SELECT node_id, name, x25519_pubkey, ed25519_pubkey FROM my_nodes;
-- Список каналов с последним сообщением
SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at
FROM channels c
JOIN channel_members cm ON c.channel_id = cm.channel_id
WHERE cm.node_id = (SELECT node_id FROM local_identity WHERE id = 1)
ORDER BY c.last_msg_at DESC;
-- Сообщения канала (последние 50)
SELECT id, author_node_id, content, content_type, timestamp,
signature, is_outgoing, is_read
FROM messages
WHERE channel_id = ?
ORDER BY timestamp DESC
LIMIT 50;
-- Кто в канале
SELECT cm.node_id, n.name
FROM channel_members cm LEFT JOIN nodes n ON cm.node_id = n.node_id
WHERE cm.channel_id = ?;
-- DM между двумя узлами
-- channel_id = "dm:" || min(a,b) || "_" || max(a,b)
-- Реакции сообщения
SELECT node_id, emoji, signature FROM reactions WHERE message_id = ? ORDER BY created_at;
-- Вложения сообщения
SELECT id, filename, content_type, size, sha256, file_path
FROM attachments WHERE message_id = ?;
================================================================================
Примечания
================================================================================
- local_identity — одна строка (id=1), содержит x25519_privkey.
Приватный ключ получаем от utun через MSG_RSP_PRIVKEY (доступен только с localhost).
- my_nodes — другие наши устройства, только pubkey-информация (нет приватных ключей).
- Адреса и подсети (node_addresses, node_subnets) — зона ответственности utun.
Чат не занимается сетевым уровнем.
- Все timestamp: INTEGER (unix epoch). messages.timestamp — миллисекунды,
created_at/updated_at/joined_at — секунды.
- signature везде NOT NULL Ed25519 (64 bytes) — подпись автора контента:
messages : sign(edPriv, channel_id || author_node_id || content || timestamp)
reactions : sign(edPriv, message_id || node_id || emoji)
attachments: sign(edPriv, message_id || filename || content_type || size || sha256)
- channel_id для DM: "dm:nodeA_nodeB" где A < B (канонический порядок).
- channel_id для групп: "group:" + SHA256(owner_node_id || name || timestamp).
- deleted_at / soft delete не добавлены — можно добавить позже при необходимости.

167
tools/chatgui/chat_tz.txt1

@ -0,0 +1,167 @@
Архитектура чата (группового)
База данных: SQLite3 (WAL mode), chats.db
================================================================================
Схема БД (7 таблиц)
================================================================================
-- 1. my_nodes — наши собственные ноды (разные устройства / инстансы utun)
CREATE TABLE my_nodes (
node_id INTEGER PRIMARY KEY,
name TEXT NOT NULL, -- human-readable имя устройства
x25519_pubkey BLOB NOT NULL, -- 32 bytes
ed25519_pubkey BLOB, -- 32 bytes (derive из x25519 privkey)
is_current INTEGER DEFAULT 0, -- 1 = текущий инстанс (к которому подключён GUI)
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch())
);
-- 2. nodes — известные удалённые узлы (получаем через msg_transport от utun)
CREATE TABLE nodes (
node_id INTEGER PRIMARY KEY,
name TEXT,
x25519_pubkey BLOB NOT NULL, -- 32 bytes
ed25519_pubkey BLOB, -- 32 bytes (для проверки подписи пира)
is_online INTEGER DEFAULT 0,
last_seen_at INTEGER DEFAULT (unixepoch()),
created_at INTEGER DEFAULT (unixepoch()),
updated_at INTEGER DEFAULT (unixepoch())
);
-- 3. channels — каналы (DM и групповые)
CREATE TABLE channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL UNIQUE, -- "dm:nodeA_nodeB" (A<B) или "group:hash"
name TEXT NOT NULL,
owner_node_id INTEGER, -- создатель (NULL для DM)
is_dm INTEGER DEFAULT 0,
last_message TEXT,
last_msg_at INTEGER, -- unix timestamp ms
created_at INTEGER DEFAULT (unixepoch())
);
CREATE UNIQUE INDEX idx_channel_cid ON channels(channel_id);
-- 4. channel_members — участники канала
CREATE TABLE channel_members (
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
node_id INTEGER NOT NULL,
joined_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (channel_id, node_id)
);
CREATE INDEX idx_cm_node ON channel_members(node_id);
-- 5. messages — сообщения чата
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
author_node_id INTEGER NOT NULL,
content TEXT NOT NULL CHECK(length(content) <= 4096),
content_type TEXT DEFAULT 'text/plain',
timestamp INTEGER NOT NULL, -- unix milliseconds
signature BLOB CHECK(signature IS NULL OR length(signature) == 64),
is_outgoing INTEGER DEFAULT 0,
is_read INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_msg_channel_time ON messages(channel_id, timestamp);
CREATE INDEX idx_msg_author ON messages(author_node_id);
-- дедупликация: один автор не может отправить два сообщения в один ms в один канал
CREATE UNIQUE INDEX idx_msg_dedup ON messages(channel_id, author_node_id, timestamp);
-- 6. reactions — реакции на сообщения
CREATE TABLE reactions (
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
node_id INTEGER NOT NULL,
emoji TEXT NOT NULL, -- unicode codepoint или :shortcode:
signature BLOB, -- Ed25519 (64 bytes)
created_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (message_id, node_id, emoji)
);
-- 7. attachments — вложения к сообщениям
CREATE TABLE attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL, -- MIME type
size INTEGER NOT NULL, -- bytes
sha256 BLOB NOT NULL, -- 32 bytes
file_path TEXT, -- локальный путь к сохранённому файлу
signature BLOB, -- Ed25519 (64 bytes)
is_upload INTEGER DEFAULT 0, -- 0=скачано, 1=наше
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_att_msg ON attachments(message_id);
================================================================================
Основные запросы
================================================================================
-- Текущая нода
SELECT node_id, name, x25519_pubkey, ed25519_pubkey
FROM my_nodes WHERE is_current = 1;
-- Все свои ноды
SELECT node_id, name, is_current FROM my_nodes ORDER BY created_at;
-- Список каналов с последним сообщением (для текущего пользователя)
SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at
FROM channels c
JOIN channel_members cm ON c.channel_id = cm.channel_id
JOIN my_nodes mn ON cm.node_id = mn.node_id
WHERE mn.is_current = 1
ORDER BY c.last_msg_at DESC;
-- Сообщения канала (последние 50)
SELECT id, author_node_id, content, content_type, timestamp,
signature, is_outgoing, is_read
FROM messages
WHERE channel_id = ?
ORDER BY timestamp DESC
LIMIT 50;
-- Кто в канале
SELECT cm.node_id, n.name
FROM channel_members cm LEFT JOIN nodes n ON cm.node_id = n.node_id
WHERE cm.channel_id = ?;
-- DM между двумя узлами
-- channel_id = "dm:" || min(a,b) || "_" || max(a,b)
-- Реакции сообщения
SELECT node_id, emoji, signature FROM reactions WHERE message_id = ? ORDER BY created_at;
-- Вложения сообщения
SELECT id, filename, content_type, size, sha256, file_path
FROM attachments WHERE message_id = ?;
================================================================================
Примечания
================================================================================
- Адреса и подсети (node_addresses, node_subnets) — зона ответственности utun.
Чат не занимается сетевым уровнем. При необходимости utun отдаёт эту информацию
через control_server (etcpmon протокол).
- Все timestamp: INTEGER (unix epoch). messages.timestamp — миллисекунды,
created_at/updated_at/joined_at — секунды.
- signature везде Ed25519 (64 bytes) — NULL если не подписано.
- channel_id для DM: "dm:nodeA_nodeB" где A < B (канонический порядок).
- channel_id для групп: "group:" + SHA256(owner_node_id || name || timestamp).
- local_seq (из предыдущей версии) убран — дедупликация через UNIQUE индекс
(channel_id, author_node_id, timestamp) достаточна.
- deleted_at / soft delete не добавлены — можно добавить позже при необходимости.

635
tools/chatgui/db/db_manager.cpp

@ -1,5 +1,8 @@
#include "db_manager.h"
#include "../src/debug_ui.h"
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <cstring>
DbManager::DbManager(const QString& path, QObject* parent)
: QObject(parent)
@ -11,106 +14,632 @@ DbManager::DbManager(const QString& path, QObject* parent)
m_db = nullptr;
return;
}
sqlite3_exec(m_db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr);
sqlite3_exec(m_db, "PRAGMA foreign_keys=ON", nullptr, nullptr, nullptr);
m_myNodeId = 100;
initTables();
GUI_INFO("Database opened: %s", path.toUtf8().constData());
if (getChannels().isEmpty()) seedStubData();
GUI_INFO("Database opened: %s (my_node_id=%llu)", path.toUtf8().constData(),
static_cast<unsigned long long>(m_myNodeId));
}
DbManager::~DbManager() {
if (m_db) {
sqlite3_close(m_db);
m_db = nullptr;
GUI_INFO("Database closed");
if (m_db) { sqlite3_close(m_db); m_db = nullptr; }
}
/* ======================================================================== */
sqlite3_stmt* DbManager::prepareOrNull(const char* sql) const {
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
GUI_ERROR("prepare failed: %s — %s", sql, sqlite3_errmsg(m_db));
return nullptr;
}
return stmt;
}
void DbManager::initTables() {
sqlite3_exec(m_db, "PRAGMA foreign_keys = ON", nullptr, nullptr, nullptr);
QString DbManager::colText(sqlite3_stmt* stmt, int col) {
return QString::fromUtf8(
reinterpret_cast<const char*>(sqlite3_column_text(stmt, col)));
}
QByteArray DbManager::colBlob(sqlite3_stmt* stmt, int col) {
int len = sqlite3_column_bytes(stmt, col);
if (len <= 0) return {};
return QByteArray(static_cast<const char*>(sqlite3_column_blob(stmt, col)), len);
}
/* ======================================================================== */
void DbManager::initTables() {
const char* sql =
"CREATE TABLE IF NOT EXISTS local_identity ("
" id INTEGER PRIMARY KEY CHECK (id = 1),"
" node_id INTEGER NOT NULL UNIQUE,"
" name TEXT NOT NULL,"
" x25519_pubkey BLOB NOT NULL,"
" x25519_privkey BLOB,"
" ed25519_pubkey BLOB,"
" created_at INTEGER DEFAULT (unixepoch()),"
" updated_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE TABLE IF NOT EXISTS my_nodes ("
" node_id INTEGER PRIMARY KEY,"
" name TEXT NOT NULL,"
" x25519_pubkey BLOB NOT NULL,"
" ed25519_pubkey BLOB,"
" created_at TEXT DEFAULT (datetime('now')),"
" updated_at TEXT DEFAULT (datetime('now'))"
" created_at INTEGER DEFAULT (unixepoch()),"
" updated_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE TABLE IF NOT EXISTS nodes ("
" node_id INTEGER PRIMARY KEY,"
" name TEXT,"
" x25519_pubkey BLOB,"
" x25519_pubkey BLOB NOT NULL,"
" ed25519_pubkey BLOB,"
" hop_count INTEGER DEFAULT 0,"
" is_online INTEGER DEFAULT 0,"
" last_seen_ver INTEGER DEFAULT 0,"
" connectivity_json TEXT,"
" created_at TEXT DEFAULT (datetime('now')),"
" updated_at TEXT DEFAULT (datetime('now'))"
");"
"CREATE TABLE IF NOT EXISTS node_addresses ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,"
" family INTEGER NOT NULL DEFAULT 4,"
" ip BLOB NOT NULL,"
" port INTEGER NOT NULL,"
" addr_type INTEGER NOT NULL DEFAULT 0,"
" socket_type INTEGER DEFAULT 0,"
" priority INTEGER DEFAULT 0"
" last_seen_at INTEGER DEFAULT (unixepoch()),"
" created_at INTEGER DEFAULT (unixepoch()),"
" updated_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_node_addr_node ON node_addresses(node_id);"
"CREATE TABLE IF NOT EXISTS node_subnets ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,"
" family INTEGER NOT NULL DEFAULT 4,"
" network BLOB NOT NULL,"
" prefix_length INTEGER NOT NULL"
"CREATE TABLE IF NOT EXISTS accounts ("
" node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id),"
" display_name TEXT NOT NULL,"
" avatar_color TEXT DEFAULT '#4A90E2',"
" avatar_letter TEXT NOT NULL,"
" is_contact INTEGER DEFAULT 1,"
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_subnet_node ON node_subnets(node_id);"
"CREATE TABLE IF NOT EXISTS channels ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" channel_id TEXT NOT NULL UNIQUE,"
" name TEXT NOT NULL,"
" member_ids_json TEXT NOT NULL,"
" owner_node_id INTEGER,"
" is_dm INTEGER DEFAULT 0,"
" last_message TEXT,"
" last_msg_at TEXT,"
" created_at TEXT DEFAULT (datetime('now'))"
" last_msg_at INTEGER,"
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_cid ON channels(channel_id);"
"CREATE TABLE IF NOT EXISTS channel_members ("
" channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,"
" node_id INTEGER NOT NULL,"
" joined_at INTEGER DEFAULT (unixepoch()),"
" PRIMARY KEY (channel_id, node_id)"
");"
"CREATE INDEX IF NOT EXISTS idx_cm_node ON channel_members(node_id);"
"CREATE TABLE IF NOT EXISTS messages ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,"
" author_node_id INTEGER NOT NULL,"
" content TEXT NOT NULL,"
" content TEXT NOT NULL CHECK(length(content) <= 4096),"
" content_type TEXT DEFAULT 'text/plain',"
" timestamp INTEGER NOT NULL,"
" local_seq INTEGER DEFAULT 0,"
" signature BLOB,"
" signature BLOB NOT NULL CHECK(length(signature) == 64),"
" is_outgoing INTEGER DEFAULT 0,"
" is_read INTEGER DEFAULT 0,"
" created_at TEXT DEFAULT (datetime('now'))"
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_msg_channel_time ON messages(channel_id, timestamp);"
"CREATE INDEX IF NOT EXISTS idx_msg_author ON messages(author_node_id);"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_dedup ON messages(channel_id, author_node_id, timestamp);";
"CREATE INDEX IF NOT EXISTS idx_msg_author ON messages(author_node_id);"
"CREATE UNIQUE INDEX IF NOT EXISTS idx_msg_dedup "
" ON messages(channel_id, author_node_id, timestamp);"
char* errMsg = nullptr;
int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &errMsg);
if (rc != SQLITE_OK) {
GUI_ERROR("Failed to create tables: %s", errMsg);
sqlite3_free(errMsg);
return;
"CREATE TABLE IF NOT EXISTS reactions ("
" message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,"
" node_id INTEGER NOT NULL,"
" emoji TEXT NOT NULL,"
" signature BLOB NOT NULL CHECK(length(signature) == 64),"
" created_at INTEGER DEFAULT (unixepoch()),"
" PRIMARY KEY (message_id, node_id, emoji)"
");"
"CREATE TABLE IF NOT EXISTS attachments ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,"
" filename TEXT NOT NULL,"
" content_type TEXT NOT NULL,"
" size INTEGER NOT NULL,"
" sha256 BLOB NOT NULL,"
" file_path TEXT,"
" signature BLOB NOT NULL CHECK(length(signature) == 64),"
" is_upload INTEGER DEFAULT 0,"
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_att_msg ON attachments(message_id);"
"CREATE TABLE IF NOT EXISTS ui_state ("
" key TEXT PRIMARY KEY,"
" value TEXT"
");"
"CREATE TABLE IF NOT EXISTS bucket_meta ("
" channel_id TEXT NOT NULL,"
" day_unix INTEGER NOT NULL,"
" hour INTEGER NOT NULL,"
" msg_count INTEGER DEFAULT 0,"
" sig_hash BLOB DEFAULT '',"
" PRIMARY KEY (channel_id, day_unix, hour)"
");"
"CREATE TABLE IF NOT EXISTS node_addresses ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,"
" family INTEGER NOT NULL CHECK(family IN (4, 6)),"
" address BLOB NOT NULL,"
" port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535),"
" is_nat INTEGER DEFAULT 0,"
" created_at INTEGER DEFAULT (unixepoch()),"
" updated_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);";
char* err = nullptr;
if (sqlite3_exec(m_db, sql, nullptr, nullptr, &err) != SQLITE_OK) {
GUI_ERROR("initTables: %s", err);
sqlite3_free(err);
} else {
GUI_INFO("Database tables initialized (11 tables)");
}
}
/* ======================================================================== */
QList<ChannelRow> DbManager::getChannels() const {
QList<ChannelRow> list;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at"
" FROM channels c JOIN channel_members cm ON c.channel_id = cm.channel_id"
" WHERE cm.node_id = ? ORDER BY c.last_msg_at DESC");
if (!stmt) return list;
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(m_myNodeId));
while (sqlite3_step(stmt) == SQLITE_ROW) {
ChannelRow r;
r.channelId = colText(stmt, 0);
r.name = colText(stmt, 1);
r.isDm = sqlite3_column_int(stmt, 2) != 0;
r.lastMessage = colText(stmt, 3);
r.lastMsgAt = sqlite3_column_int64(stmt, 4);
list.append(r);
}
sqlite3_finalize(stmt);
return list;
}
QList<MessageRow> DbManager::getMessages(const QString& channelId, int limit) const {
QList<MessageRow> list;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT id, author_node_id, content, content_type, timestamp,"
" signature, is_outgoing, is_read"
" FROM messages WHERE channel_id = ? ORDER BY timestamp ASC LIMIT ?");
if (!stmt) return list;
sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_int(stmt, 2, limit);
while (sqlite3_step(stmt) == SQLITE_ROW) {
MessageRow m;
m.id = sqlite3_column_int64(stmt, 0);
m.channelId = channelId;
m.authorNodeId = static_cast<quint64>(sqlite3_column_int64(stmt, 1));
m.content = colText(stmt, 2);
m.contentType = colText(stmt, 3);
m.timestamp = sqlite3_column_int64(stmt, 4);
m.signature = colBlob(stmt, 5);
m.isOutgoing = sqlite3_column_int(stmt, 6) != 0;
m.isRead = sqlite3_column_int(stmt, 7) != 0;
list.append(m);
}
sqlite3_finalize(stmt);
return list;
}
QList<AccountRow> DbManager::getAccounts(bool contactsOnly) const {
QList<AccountRow> list;
const char* sql = contactsOnly
? "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact"
" FROM accounts WHERE is_contact = 1 ORDER BY display_name"
: "SELECT node_id, display_name, avatar_color, avatar_letter, is_contact"
" FROM accounts ORDER BY display_name";
sqlite3_stmt* stmt = prepareOrNull(sql);
if (!stmt) return list;
while (sqlite3_step(stmt) == SQLITE_ROW) {
AccountRow a;
a.nodeId = static_cast<quint64>(sqlite3_column_int64(stmt, 0));
a.displayName = colText(stmt, 1);
a.avatarColor = colText(stmt, 2);
a.avatarLetter = colText(stmt, 3);
a.isContact = sqlite3_column_int(stmt, 4) != 0;
list.append(a);
}
sqlite3_finalize(stmt);
return list;
}
AccountRow DbManager::getAccount(quint64 nodeId) const {
AccountRow a;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT node_id, display_name, avatar_color, avatar_letter, is_contact"
" FROM accounts WHERE node_id = ?");
if (!stmt) return a;
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(nodeId));
if (sqlite3_step(stmt) == SQLITE_ROW) {
a.nodeId = static_cast<quint64>(sqlite3_column_int64(stmt, 0));
a.displayName = colText(stmt, 1);
a.avatarColor = colText(stmt, 2);
a.avatarLetter = colText(stmt, 3);
a.isContact = sqlite3_column_int(stmt, 4) != 0;
}
sqlite3_finalize(stmt);
return a;
}
QByteArray DbManager::getNodeEdPub(quint64 nodeId) const {
sqlite3_stmt* stmt = prepareOrNull(
"SELECT ed25519_pubkey FROM nodes WHERE node_id=?");
if (!stmt) return {};
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(nodeId));
QByteArray r;
if (sqlite3_step(stmt) == SQLITE_ROW)
r = colBlob(stmt, 0);
sqlite3_finalize(stmt);
return r;
}
QList<DbManager::BucketMeta> DbManager::getBucketMeta(const QString& channelId) const {
QList<BucketMeta> list;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT day_unix, hour, msg_count, sig_hash"
" FROM bucket_meta WHERE channel_id=? ORDER BY day_unix, hour");
if (!stmt) return list;
sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC);
while (sqlite3_step(stmt) == SQLITE_ROW) {
BucketMeta b;
b.day = sqlite3_column_int64(stmt, 0);
b.hour = sqlite3_column_int(stmt, 1);
b.count = sqlite3_column_int(stmt, 2);
b.hash = colBlob(stmt, 3);
list.append(b);
}
sqlite3_finalize(stmt);
return list;
}
void DbManager::setBucketMeta(const QString& channelId, qint64 day, int hour,
int count, const QByteArray& hash) {
sqlite3_stmt* stmt = prepareOrNull(
"INSERT OR REPLACE INTO bucket_meta(channel_id,day_unix,hour,msg_count,sig_hash)"
" VALUES(?,?,?,?,?)");
if (!stmt) return;
sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, day);
sqlite3_bind_int(stmt, 3, hour);
sqlite3_bind_int(stmt, 4, count);
sqlite3_bind_blob(stmt, 5, hash.constData(), hash.size(), SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
QList<QByteArray> DbManager::getMessageSigs(const QString& channelId,
qint64 day, int hour) const {
QList<QByteArray> sigs;
qint64 dayStart = day + hour * 3600;
qint64 dayEnd = dayStart + 3600;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT signature FROM messages WHERE channel_id=?"
" AND timestamp/1000 >= ? AND timestamp/1000 < ? ORDER BY timestamp");
if (!stmt) return sigs;
sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, dayStart);
sqlite3_bind_int64(stmt, 3, dayEnd);
while (sqlite3_step(stmt) == SQLITE_ROW)
sigs.append(colBlob(stmt, 0));
sqlite3_finalize(stmt);
return sigs;
}
QList<MessageRow> DbManager::getMessagesBySigs(const QList<QByteArray>& sigs) const {
QList<MessageRow> msgs;
if (sigs.isEmpty()) return msgs;
QString sql = "SELECT id, channel_id, author_node_id, content, content_type,"
" timestamp, signature, is_outgoing, is_read FROM messages"
" WHERE signature IN (";
for (int i = 0; i < sigs.size(); i++)
sql += (i > 0 ? ",?" : "?");
sql += ") ORDER BY timestamp";
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return msgs;
for (int i = 0; i < sigs.size(); i++)
sqlite3_bind_blob(stmt, i + 1, sigs[i].constData(), sigs[i].size(), SQLITE_STATIC);
while (sqlite3_step(stmt) == SQLITE_ROW) {
MessageRow m;
m.id = sqlite3_column_int64(stmt, 0);
m.channelId = colText(stmt, 1);
m.authorNodeId = static_cast<quint64>(sqlite3_column_int64(stmt, 2));
m.content = colText(stmt, 3);
m.contentType = colText(stmt, 4);
m.timestamp = sqlite3_column_int64(stmt, 5);
m.signature = colBlob(stmt, 6);
m.isOutgoing = sqlite3_column_int(stmt, 7) != 0;
m.isRead = sqlite3_column_int(stmt, 8) != 0;
msgs.append(m);
}
sqlite3_finalize(stmt);
return msgs;
}
QList<quint64> DbManager::getOnlinePeers() const {
QList<quint64> peers;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT node_id FROM nodes WHERE is_online=1 AND node_id != ?");
if (!stmt) return peers;
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(m_myNodeId));
while (sqlite3_step(stmt) == SQLITE_ROW)
peers.append(static_cast<quint64>(sqlite3_column_int64(stmt, 0)));
sqlite3_finalize(stmt);
return peers;
}
bool DbManager::insertMessage(const QString& channelId, quint64 authorNodeId,
const QString& content, qint64 timestamp, int isOutgoing) {
static const unsigned char sig64[64] = {};
sqlite3_stmt* stmt = prepareOrNull(
"INSERT INTO messages"
" (channel_id,author_node_id,content,timestamp,signature,is_outgoing,is_read)"
" VALUES(?,?,?,?,?,?,1)");
if (!stmt) return false;
sqlite3_bind_text(stmt, 1, channelId.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 2, static_cast<sqlite3_int64>(authorNodeId));
sqlite3_bind_text(stmt, 3, content.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 4, timestamp);
sqlite3_bind_blob(stmt, 5, sig64, 64, SQLITE_STATIC);
sqlite3_bind_int(stmt, 6, isOutgoing);
bool ok = (sqlite3_step(stmt) == SQLITE_DONE);
sqlite3_finalize(stmt);
/* update bucket_meta */
qint64 day = timestamp / 86400000; /* ms per day */
int hour = static_cast<int>((timestamp % 86400000) / 3600000);
auto sigs = getMessageSigs(channelId, day, hour);
int cnt = sigs.size();
QByteArray hash(32, '\0');
if (cnt > 0) {
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
EVP_DigestInit_ex(md_ctx, EVP_sha256(), NULL);
for (auto& s : sigs)
EVP_DigestUpdate(md_ctx, s.constData(), s.size());
unsigned int len = SHA256_DIGEST_LENGTH;
uint8_t buf[SHA256_DIGEST_LENGTH];
EVP_DigestFinal_ex(md_ctx, buf, &len);
EVP_MD_CTX_free(md_ctx);
hash = QByteArray(reinterpret_cast<const char*>(buf), static_cast<int>(len));
}
setBucketMeta(channelId, day, hour, cnt, hash);
return ok;
}
QByteArray DbManager::getLocalPubkey() const {
sqlite3_stmt* stmt = prepareOrNull(
"SELECT x25519_pubkey FROM local_identity WHERE id=1");
if (!stmt) return {};
QByteArray r;
if (sqlite3_step(stmt) == SQLITE_ROW)
r = colBlob(stmt, 0);
sqlite3_finalize(stmt);
return r;
}
QList<NodeAddr> DbManager::getOnlinePeerAddresses(quint64 excludeNodeId) const {
QList<NodeAddr> list;
sqlite3_stmt* stmt = prepareOrNull(
"SELECT na.node_id, na.family, na.address, na.port"
" FROM node_addresses na JOIN nodes n ON na.node_id = n.node_id"
" WHERE n.is_online=1 AND na.is_nat=0 AND na.node_id != ?"
" ORDER BY RANDOM()");
if (!stmt) return list;
sqlite3_bind_int64(stmt, 1, static_cast<sqlite3_int64>(excludeNodeId));
while (sqlite3_step(stmt) == SQLITE_ROW) {
NodeAddr a;
a.nodeId = static_cast<quint64>(sqlite3_column_int64(stmt, 0));
a.family = sqlite3_column_int(stmt, 1);
a.address = colBlob(stmt, 2);
a.port = static_cast<quint16>(sqlite3_column_int(stmt, 3));
list.append(a);
}
sqlite3_finalize(stmt);
return list;
}
/* ======================================================================== */
static constexpr unsigned char kDummySig[64] = {
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,
0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,
0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,
0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,
0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,
0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,
0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F
};
void DbManager::seedStubData() {
GUI_INFO("Seeding stub data...");
sqlite3_exec(m_db, "BEGIN", nullptr, nullptr, nullptr);
/* my_node dummy pubkey */
unsigned char pub_dummy[32] = {};
sqlite3_stmt* ins = prepareOrNull(
"INSERT OR IGNORE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey)"
" VALUES(1,?,'MyDevice',?,?)");
sqlite3_bind_int64(ins, 1, m_myNodeId);
sqlite3_bind_blob(ins, 2, pub_dummy, 32, SQLITE_STATIC);
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC);
sqlite3_step(ins); sqlite3_finalize(ins);
/* nodes + accounts */
struct { quint64 id; const char* name; const char* letter; const char* color; } accs[] = {
{1, "Alice", "A", "#4A90E2"}, {2, "Bob", "B", "#7ED321"},
{3, "Carol", "C", "#F5A623"}, {4, "Dave", "D", "#9013FE"},
{5, "Eve", "E", "#00B894"},
};
for (auto& a : accs) {
ins = prepareOrNull("INSERT OR IGNORE INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey)"
" VALUES(?,?,?,?)");
sqlite3_bind_int64(ins, 1, static_cast<sqlite3_int64>(a.id));
sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_STATIC);
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC);
sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_STATIC);
sqlite3_step(ins); sqlite3_finalize(ins);
ins = prepareOrNull("INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter)"
" VALUES(?,?,?,?)");
sqlite3_bind_int64(ins, 1, static_cast<sqlite3_int64>(a.id));
sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 3, a.color, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 4, a.letter, -1, SQLITE_STATIC);
sqlite3_step(ins); sqlite3_finalize(ins);
}
/* set some nodes online for demo */
sqlite3_exec(m_db,
"UPDATE nodes SET is_online=1 WHERE node_id IN (1,2,3)", nullptr, nullptr, nullptr);
/* node addresses for online peers (stub, for invite link demo) */
struct { quint64 id; int fam; unsigned char addr[16]; int port; int nat; } addrs[] = {
{1, 4, {192,168,1,10}, 8001, 0},
{1, 6, {0xfd,0x12,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, 8001, 0},
{2, 4, {10,0,0,5}, 9001, 0},
{2, 4, {172,16,0,100}, 9001, 0},
{3, 4, {192,168,5,20}, 10001, 1}, /* Carol behind NAT */
};
for (auto& ad : addrs) {
ins = prepareOrNull("INSERT OR IGNORE INTO node_addresses(node_id,family,address,port,is_nat)"
" VALUES(?,?,?,?,?)");
sqlite3_bind_int64(ins, 1, static_cast<sqlite3_int64>(ad.id));
sqlite3_bind_int(ins, 2, ad.fam);
sqlite3_bind_blob(ins, 3, ad.addr, ad.fam == 4 ? 4 : 16, SQLITE_STATIC);
sqlite3_bind_int(ins, 4, ad.port);
sqlite3_bind_int(ins, 5, ad.nat);
sqlite3_step(ins); sqlite3_finalize(ins);
}
/* channels (5) */
struct { const char* cid; const char* name; } chs[] = {
{"ch:general", "#general"},
{"ch:random", "#random"},
{"ch:dev-team", "#dev-team"},
{"ch:announce", "#announcements"},
{"ch:offtopic", "#offtopic"},
};
for (auto& ch : chs) {
ins = prepareOrNull("INSERT OR IGNORE INTO channels(channel_id,name) VALUES(?,?)");
sqlite3_bind_text(ins, 1, ch.cid, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 2, ch.name, -1, SQLITE_STATIC);
sqlite3_step(ins); sqlite3_finalize(ins);
/* my node member */
ins = prepareOrNull("INSERT OR IGNORE INTO channel_members(channel_id,node_id) VALUES(?,?)");
sqlite3_bind_text(ins, 1, ch.cid, -1, SQLITE_STATIC);
sqlite3_bind_int64(ins, 2, static_cast<sqlite3_int64>(m_myNodeId));
sqlite3_step(ins); sqlite3_finalize(ins);
/* all 5 accounts as members (everyone in every channel for demo) */
for (auto& a : accs) {
ins = prepareOrNull("INSERT OR IGNORE INTO channel_members(channel_id,node_id) VALUES(?,?)");
sqlite3_bind_text(ins, 1, ch.cid, -1, SQLITE_STATIC);
sqlite3_bind_int64(ins, 2, static_cast<sqlite3_int64>(a.id));
sqlite3_step(ins); sqlite3_finalize(ins);
}
}
/* messages (8) */
struct { const char* cid; quint64 author; qint64 ts; const char* text; quint8 o; } msgs[] = {
{"ch:general", 1, 1719200100000, "Hey team! How's the deployment going?", 0},
{"ch:general", 2, 1719200220000, "All green on staging. Running the final integration tests now.", 0},
{"ch:general", 3, 1719200520000, "I've pushed the ETCP-42 fix to staging. BGP handshake should be stable now. Please review the patch when you get a chance.", 0},
{"ch:general", 4, 1719200700000, "Wait — the BGP session is still flapping after the handshake. I'm seeing the keepalive timer fire but no route updates come through. Logs attached.", 0},
{"ch:general", 1, 1719200880000, "Reviewed the patch. LGTM. Let's merge and deploy to production tomorrow morning.", 0},
{"ch:general", 2, 1719201000000, "Can you share the full logs for the BGP session? I'll dig into the keepalive timer logic.", 0},
{"ch:general", 3, 1719201180000, "Sounds good. Let's coordinate in #dev-team. I'll prepare the release notes.", 0},
{"ch:general", 1, 1719201300000, "Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main.", 0},
};
for (auto& m : msgs) {
ins = prepareOrNull(
"INSERT OR IGNORE INTO messages"
" (channel_id,author_node_id,content,timestamp,signature,is_outgoing,is_read)"
" VALUES(?,?,?,?,?,?,1)");
sqlite3_bind_text(ins, 1, m.cid, -1, SQLITE_STATIC);
sqlite3_bind_int64(ins, 2, static_cast<sqlite3_int64>(m.author));
sqlite3_bind_text(ins, 3, m.text, -1, SQLITE_STATIC);
sqlite3_bind_int64(ins, 4, m.ts);
sqlite3_bind_blob(ins, 5, kDummySig, 64, SQLITE_STATIC);
sqlite3_bind_int(ins, 6, m.o);
sqlite3_step(ins); sqlite3_finalize(ins);
}
/* reactions */
struct { qint64 msgRowId; quint64 node; const char* emoji; } reacts[] = {
{2, 1, "\xF0\x9F\x91\x8D"}, {2, 3, "\xF0\x9F\x91\x8D"},
{2, 1, "\xF0\x9F\x91\x8B"}, {2, 3, "\xF0\x9F\x91\x8B"}, {2, 4, "\xF0\x9F\x91\x8B"},
{3, 1, "\xF0\x9F\x91\x8D"}, {3, 2, "\xF0\x9F\x91\x8D"}, {3, 4, "\xF0\x9F\x91\x8D"},
{3, 1, "\xE2\x9D\xA4"},
{4, 2, "\xF0\x9F\x98\xAE"},
{5, 1, "\xF0\x9F\x91\x8D"}, {5, 2, "\xF0\x9F\x91\x8D"}, {5, 3, "\xF0\x9F\x91\x8D"},
{5, 4, "\xF0\x9F\x91\x8D"}, {5, 5, "\xF0\x9F\x91\x8D"},
{5, 2, "\xF0\x9F\x8E\x89"}, {5, 5, "\xF0\x9F\x8E\x89"},
{7, 1, "\xF0\x9F\xA4\x96"}, {7, 2, "\xF0\x9F\xA4\x96"},
{8, 2, "\xF0\x9F\x9A\x80"}, {8, 3, "\xF0\x9F\x9A\x80"}, {8, 4, "\xF0\x9F\x9A\x80"},
{8, 1, "\xE2\x9D\xA4"}, {8, 2, "\xE2\x9D\xA4"}, {8, 3, "\xE2\x9D\xA4"},
{8, 4, "\xE2\x9D\xA4"}, {8, 5, "\xE2\x9D\xA4"}, {8, 1, "\xE2\x9D\xA4"}, {8, 1, "\xE2\x9D\xA4"},
};
std::string lastMsg;
qint64 lastTs = 0;
for (auto& r : reacts) {
ins = prepareOrNull(
"INSERT OR IGNORE INTO reactions(message_id,node_id,emoji,signature)"
" VALUES(?,?,?,?)");
sqlite3_bind_int64(ins, 1, r.msgRowId);
sqlite3_bind_int64(ins, 2, static_cast<sqlite3_int64>(r.node));
sqlite3_bind_text(ins, 3, r.emoji, -1, SQLITE_STATIC);
sqlite3_bind_blob(ins, 4, kDummySig, 64, SQLITE_STATIC);
sqlite3_step(ins); sqlite3_finalize(ins);
}
lastMsg = "Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main.";
lastTs = 1719201300000;
/* update channel last_msg */
ins = prepareOrNull("UPDATE channels SET last_message=?, last_msg_at=? WHERE channel_id='ch:general'");
sqlite3_bind_text(ins, 1, lastMsg.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_int64(ins, 2, lastTs);
sqlite3_step(ins); sqlite3_finalize(ins);
sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr);
GUI_INFO("Seed data inserted (5 accounts, 5 channels, 8 messages, %d reactions, %d addresses)",
static_cast<int>(sizeof(reacts)/sizeof(reacts[0])),
static_cast<int>(sizeof(addrs)/sizeof(addrs[0])));
}
QString DbManager::getUiState(const QString& key) const {
sqlite3_stmt* stmt = prepareOrNull("SELECT value FROM ui_state WHERE key=?");
if (!stmt) return {};
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_STATIC);
QString val;
if (sqlite3_step(stmt) == SQLITE_ROW)
val = colText(stmt, 0);
sqlite3_finalize(stmt);
return val;
}
GUI_INFO("Database tables initialized (6 tables, 6 indexes)");
void DbManager::setUiState(const QString& key, const QString& value) {
sqlite3_stmt* stmt = prepareOrNull(
"INSERT OR REPLACE INTO ui_state(key,value) VALUES(?,?)");
if (!stmt) return;
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, value.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}

67
tools/chatgui/db/db_manager.h

@ -3,7 +3,44 @@
#include <sqlite3.h>
#include <QString>
#include <QList>
#include <QObject>
#include <cstdint>
struct ChannelRow {
QString channelId;
QString name;
bool isDm = false;
QString lastMessage;
qint64 lastMsgAt = 0;
};
struct MessageRow {
qint64 id = 0;
QString channelId;
quint64 authorNodeId = 0;
QString content;
QString contentType;
qint64 timestamp = 0;
QByteArray signature;
bool isOutgoing = false;
bool isRead = false;
};
struct AccountRow {
quint64 nodeId = 0;
QString displayName;
QString avatarColor;
QString avatarLetter;
bool isContact = true;
};
struct NodeAddr {
quint64 nodeId = 0;
int family = 0; // 4=IPv4, 6=IPv6
QByteArray address;
quint16 port = 0;
};
class DbManager : public QObject {
Q_OBJECT
@ -12,12 +49,40 @@ public:
~DbManager() override;
bool isOpen() const { return m_db != nullptr; }
sqlite3* handle() { return m_db; }
quint64 myNodeId() const { return m_myNodeId; }
QList<ChannelRow> getChannels() const;
QList<MessageRow> getMessages(const QString& channelId, int limit = 50) const;
QList<AccountRow> getAccounts(bool contactsOnly = false) const;
AccountRow getAccount(quint64 nodeId) const;
QByteArray getNodeEdPub(quint64 nodeId) const;
struct BucketMeta { qint64 day; int hour; int count; QByteArray hash; };
QList<BucketMeta> getBucketMeta(const QString& channelId) const;
void setBucketMeta(const QString& channelId, qint64 day, int hour,
int count, const QByteArray& hash);
QList<QByteArray> getMessageSigs(const QString& channelId, qint64 day, int hour) const;
QList<MessageRow> getMessagesBySigs(const QList<QByteArray>& sigs) const;
QList<quint64> getOnlinePeers() const;
QByteArray getLocalPubkey() const;
QList<NodeAddr> getOnlinePeerAddresses(quint64 excludeNodeId) const;
void seedStubData();
bool insertMessage(const QString& channelId, quint64 authorNodeId,
const QString& content, qint64 timestamp, int isOutgoing);
QString getUiState(const QString& key) const;
void setUiState(const QString& key, const QString& value);
private:
void initTables();
sqlite3_stmt* prepareOrNull(const char* sql) const;
static QString colText(sqlite3_stmt* stmt, int col);
static QByteArray colBlob(sqlite3_stmt* stmt, int col);
sqlite3* m_db = nullptr;
quint64 m_myNodeId = 0;
};
#endif

19
tools/chatgui/doc/db_schema.md

@ -1,5 +1,8 @@
# База данных децентрализованного чата (chatgui)
Сторона сервиса (gui):
-
## Архитектура хранения
Чат **децентрализованный** — нет центрального сервера. Каждый узел (нода uTun) хранит свою
@ -14,24 +17,32 @@
## Структура таблиц SQLite
### `local_identity` — наш собственный узел (точнее все мои устройства)
### `local_identity` — наш собственный узел
### `my_nodes` — все мои устройства)
между моими устройствами синхронизируется контент - список чатов и сообщения.
... todo
```sql
CREATE TABLE IF NOT EXISTS nodes (
id INTEGER PRIMARY KEY,
node_id INTEGER NOT NULL UNIQUE, // id из utun
name TEXT NOT NULL, // ник
pubkey (ed+x)
last_seen INTEGER DEFAULT 0,
online_rating INTEGER DEFAULT 0, // рейттинг связности с узлом. чем больше число тем чаще узел онлайн и с ним хорошая связь
online_rating INTEGER DEFAULT 0, // рейтинг доступности узла. чем больше число тем чаще узел онлайн
speed_rating INTEGER DEFAULT 0, // рейтинг скорости обмена с узлом
rtt_rating INTEGER DEFAULT 0, // rtt до узла (через промежуточные узлы если есть)
connectivity_json TEXT, // как связаться с узлом
connectivity_json TEXT, // как связаться с узлом - адреса, (пока пусто)
parent_node_id INTEGER DEFAULT NULL, // каждая нода должна иметь родителя (кроме корневой)
main_node_id INTEGER DEFAULT NULL, // если несколько устройств у клиента, id главной ноды
main_node_id INTEGER DEFAULT NULL, // если несколько устройств у клиента, id следующей ноды (циклический список)
created_at TEXT DEFAULT (datetime('now')),
);
```
### `channels` — чаты (группы узлов)
```sql

28
tools/chatgui/src/accountlist.cpp

@ -1,4 +1,5 @@
#include "accountlist.h"
#include "../db/db_manager.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QPainter>
@ -22,10 +23,11 @@ static QPixmap makeAvatar(const QColor &color, const QString &letter) {
return pix;
}
AccountList::AccountList(QWidget *parent)
AccountList::AccountList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_listView(new QListView(this))
, m_model(new QStandardItemModel(this))
, m_db(db)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@ -43,27 +45,17 @@ AccountList::AccountList(QWidget *parent)
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
layout->addWidget(m_listView);
populateStubData();
}
void AccountList::populateStubData() {
struct {
const char *name;
QColor color;
const char *letter;
} accounts[] = {
{"Alice", QColor(74, 144, 226), "A"},
{"Bob", QColor(126, 211, 33), "B"},
{"Carol", QColor(245, 166, 35), "C"},
{"Dave", QColor(144, 19, 254), "D"},
{"Eve", QColor(0, 184, 148), "E"},
};
void AccountList::loadAccounts() {
m_model->clear();
if (!m_db || !m_db->isOpen()) return;
for (const auto &a : accounts) {
auto accounts = m_db->getAccounts(true);
for (const auto& a : accounts) {
auto *item = new QStandardItem();
item->setText(a.name);
item->setIcon(QIcon(makeAvatar(a.color, a.letter)));
item->setText(a.displayName);
item->setIcon(QIcon(makeAvatar(QColor(a.avatarColor), a.avatarLetter)));
m_model->appendRow(item);
}
}

10
tools/chatgui/src/accountlist.h

@ -4,13 +4,17 @@
#include <QListView>
#include <QStandardItemModel>
class DbManager;
class AccountList : public QWidget {
Q_OBJECT
public:
explicit AccountList(QWidget *parent = nullptr);
explicit AccountList(DbManager* db = nullptr, QWidget *parent = nullptr);
void loadAccounts();
private:
QListView *m_listView;
QListView *m_listView;
QStandardItemModel *m_model;
void populateStubData();
DbManager *m_db;
};

7
tools/chatgui/src/channeldelegate.h

@ -3,9 +3,10 @@
#include <QStyledItemDelegate>
enum ChannelDataRole {
ChannelNameRole = Qt::DisplayRole,
ChannelIconRole = Qt::DecorationRole,
ChannelLastMessageRole = Qt::UserRole + 1
ChannelNameRole = Qt::DisplayRole,
ChannelIconRole = Qt::DecorationRole,
ChannelLastMessageRole = Qt::UserRole + 1,
ChannelChannelIdRole = Qt::UserRole + 2
};
class ChannelDelegate : public QStyledItemDelegate {

69
tools/chatgui/src/channellist.cpp

@ -1,9 +1,11 @@
#include "channellist.h"
#include "channeldelegate.h"
#include "../db/db_manager.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QPainter>
#include <QPixmap>
#include <QMenu>
static QIcon makeChannelIcon(const QColor &color, const QString &letter) {
QPixmap pix(32, 32);
@ -23,10 +25,11 @@ static QIcon makeChannelIcon(const QColor &color, const QString &letter) {
return QIcon(pix);
}
ChannelList::ChannelList(QWidget *parent)
ChannelList::ChannelList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_listView(new QListView(this))
, m_model(new QStandardItemModel(this))
, m_db(db)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@ -43,40 +46,62 @@ ChannelList::ChannelList(QWidget *parent)
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
layout->addWidget(m_listView);
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
this, [this](const QModelIndex &current, const QModelIndex &) {
if (current.isValid()) {
emit channelSelected(current.row());
QString cid = current.data(ChannelChannelIdRole).toString();
emit channelSelected(cid);
}
});
populateStubData();
connect(m_listView, &QListView::customContextMenuRequested,
this, &ChannelList::onContextMenu);
}
void ChannelList::populateStubData() {
struct {
const char *name;
const char *lastMsg;
QColor color;
const char *letter;
} channels[] = {
{"#general", "Alice: Sounds good, let's deploy tomorrow morning.", QColor(74, 144, 226), "G"},
{"#random", "Bob: Anyone seen the latest episode? It was amazing!", QColor(126, 211, 33), "R"},
{"#dev-team", "Carol: I've pushed the fix for ETCP-42 to staging.", QColor(245, 166, 35), "D"},
{"#announcements","Admin: Maintenance scheduled for Sunday 02:00 UTC.", QColor(208, 2, 27), "A"},
{"#support", "Dave: Can someone help me configure the routing table? The BGP session keeps dropping after the handshake completes. I've checked the logs.", QColor(144, 19, 254), "S"},
{"#offtopic", "Eve: Check out this new keyboard — Cherry MX Blue, 60%.", QColor(0, 184, 148), "O"},
void ChannelList::loadChannels() {
m_model->clear();
if (!m_db || !m_db->isOpen()) return;
auto channels = m_db->getChannels();
static const QColor colors[] = {
QColor(74,144,226), QColor(126,211,33), QColor(245,166,35),
QColor(208,2,27), QColor(144,19,254), QColor(0,184,148)
};
int ci = 0;
for (const auto &ch : channels) {
for (const auto& ch : channels) {
auto letter = ch.name.mid(0, 1).toUpper();
if (letter.isEmpty()) letter = "#";
auto *item = new QStandardItem();
item->setData(ch.name, ChannelNameRole);
item->setData(QString::fromUtf8(ch.lastMsg), ChannelLastMessageRole);
item->setData(makeChannelIcon(ch.color, ch.letter), ChannelIconRole);
item->setData(ch.name, ChannelNameRole);
item->setData(ch.channelId, ChannelChannelIdRole);
item->setData(ch.lastMessage, ChannelLastMessageRole);
item->setData(makeChannelIcon(colors[ci % 6], letter), ChannelIconRole);
m_model->appendRow(item);
ci++;
}
m_listView->setCurrentIndex(m_model->index(0, 0));
if (m_model->rowCount() > 0)
m_listView->setCurrentIndex(m_model->index(0, 0));
}
void ChannelList::selectChannel(const QString& channelId) {
for (int row = 0; row < m_model->rowCount(); row++) {
if (m_model->item(row)->data(ChannelChannelIdRole).toString() == channelId) {
m_listView->setCurrentIndex(m_model->index(row, 0));
return;
}
}
}
void ChannelList::onContextMenu(const QPoint& pos) {
QModelIndex index = m_listView->indexAt(pos);
if (!index.isValid()) return;
QString cid = index.data(ChannelChannelIdRole).toString();
QMenu menu(this);
QAction *inviteAction = menu.addAction("Пригласить");
if (menu.exec(m_listView->viewport()->mapToGlobal(pos)) == inviteAction)
emit inviteRequested(cid);
}

18
tools/chatgui/src/channellist.h

@ -3,17 +3,27 @@
#include <QWidget>
#include <QListView>
#include <QStandardItemModel>
#include <QString>
class DbManager;
class ChannelList : public QWidget {
Q_OBJECT
public:
explicit ChannelList(QWidget *parent = nullptr);
explicit ChannelList(DbManager* db = nullptr, QWidget *parent = nullptr);
void loadChannels();
void selectChannel(const QString& channelId);
signals:
void channelSelected(int index);
void channelSelected(const QString& channelId);
void inviteRequested(const QString& channelId);
private slots:
void onContextMenu(const QPoint& pos);
private:
QListView *m_listView;
QListView *m_listView;
QStandardItemModel *m_model;
void populateStubData();
DbManager *m_db;
};

15
tools/chatgui/src/inputbar.cpp

@ -5,6 +5,7 @@
#include <QTextEdit>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFileDialog>
#include <QKeyEvent>
#include <QFontMetrics>
@ -23,6 +24,16 @@ static QPushButton *makeIconBtn(const QString &text, const QString &tooltip, QWi
return btn;
}
static QWidget *wrapBottomBtn(QPushButton *btn, int pad) {
auto *w = new QWidget(btn->parentWidget());
auto *l = new QVBoxLayout(w);
l->setContentsMargins(0, 0, 0, 0);
l->setSpacing(0);
l->addWidget(btn);
l->addSpacing(pad);
return w;
}
InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent)
: QWidget(parent)
, m_input(new QTextEdit(this))
@ -41,7 +52,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent)
qDebug() << "Attached:" << path;
}
});
layout->addWidget(attachBtn, 0, Qt::AlignBottom);
layout->addWidget(wrapBottomBtn(attachBtn, 3), 0, Qt::AlignBottom);
m_input->setAcceptRichText(false);
m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@ -58,7 +69,7 @@ InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent)
m_emojiBtn = makeIconBtn(QString::fromUtf8("\xF0\x9F\x99\x82"),
QString::fromUtf8("Смайлики"), this);
layout->addWidget(m_emojiBtn, 0, Qt::AlignBottom);
layout->addWidget(wrapBottomBtn(m_emojiBtn, 3), 0, Qt::AlignBottom);
auto *sendBtn = makeIconBtn(QString::fromUtf8("\xE2\x9E\xA4"),
QString::fromUtf8("Отправить"), this);

100
tools/chatgui/src/invite_link.cpp

@ -0,0 +1,100 @@
#include "invite_link.h"
#include "debug_ui.h"
#include <QByteArray>
static QByteArray encodeAddresses(const QList<NodeAddr>& addrs) {
QByteArray buf;
for (const auto& a : addrs) {
buf.append(static_cast<char>(a.family));
buf.append(a.address);
buf.append(static_cast<char>((a.port >> 8) & 0xFF));
buf.append(static_cast<char>(a.port & 0xFF));
}
return buf;
}
static QList<InviteAddr> decodeAddresses(const uint8_t* data, int len,
int count) {
QList<InviteAddr> list;
int off = 0;
for (int i = 0; i < count && off < len; i++) {
if (off + 1 > len) break;
InviteAddr a;
a.family = data[off++];
int alen = (a.family == 4) ? 4 : 16;
if (off + alen + 2 > len) break;
a.address = QByteArray(reinterpret_cast<const char*>(data + off), alen);
off += alen;
a.port = (static_cast<quint16>(data[off]) << 8) | data[off + 1];
off += 2;
list.append(a);
}
return list;
}
QString encodeInviteLink(const QString& channelId, const QByteArray& pubkey,
const QList<NodeAddr>& addrs) {
QByteArray cid = channelId.toUtf8();
if (cid.size() > 65535) {
GUI_ERROR("encodeInviteLink: channelId too long (%d bytes)", cid.size());
return {};
}
if (pubkey.size() != 32) {
GUI_ERROR("encodeInviteLink: pubkey must be 32 bytes (got %d)", pubkey.size());
return {};
}
QByteArray addrsEnc = encodeAddresses(addrs);
int addrCount = addrs.size();
if (addrCount > 65535) addrCount = 65535;
QByteArray blob;
blob.reserve(1 + 2 + cid.size() + 32 + 2 + addrsEnc.size());
blob.append(static_cast<char>(INVITE_LINK_VERSION));
blob.append(static_cast<char>((cid.size() >> 8) & 0xFF));
blob.append(static_cast<char>(cid.size() & 0xFF));
blob.append(cid);
blob.append(pubkey);
blob.append(static_cast<char>((addrCount >> 8) & 0xFF));
blob.append(static_cast<char>(addrCount & 0xFF));
blob.append(addrsEnc);
return QString::fromLatin1("utun://") + QString::fromLatin1(blob.toBase64());
}
InviteData decodeInviteLink(const QString& link) {
InviteData d;
if (!link.startsWith(QLatin1String("utun://"))) {
GUI_ERROR("decodeInviteLink: invalid prefix");
return d;
}
QByteArray blob = QByteArray::fromBase64(
link.mid(7).toLatin1());
const auto* data = reinterpret_cast<const uint8_t*>(blob.constData());
int len = blob.size();
if (len < 5) {
GUI_ERROR("decodeInviteLink: blob too short (%d bytes)", len);
return d;
}
int off = 0;
uint8_t ver = data[off++];
if (ver != INVITE_LINK_VERSION) {
GUI_ERROR("decodeInviteLink: unsupported version %d", ver);
return d;
}
int cidLen = (static_cast<int>(data[off]) << 8) | data[off + 1];
off += 2;
if (off + cidLen + 32 + 2 > len) {
GUI_ERROR("decodeInviteLink: truncated blob");
return d;
}
d.channelId = QString::fromUtf8(reinterpret_cast<const char*>(data + off), cidLen);
off += cidLen;
d.pubkey = QByteArray(reinterpret_cast<const char*>(data + off), 32);
off += 32;
int addrCount = (static_cast<int>(data[off]) << 8) | data[off + 1];
off += 2;
d.addrs = decodeAddresses(data + off, len - off, addrCount);
return d;
}

26
tools/chatgui/src/invite_link.h

@ -0,0 +1,26 @@
#pragma once
#include <QString>
#include <QByteArray>
#include <QList>
#include <cstdint>
#include "../db/db_manager.h"
#define INVITE_LINK_VERSION 0x01
struct InviteAddr {
int family; // 4=IPv4, 6=IPv6
QByteArray address;
quint16 port;
};
struct InviteData {
QString channelId;
QByteArray pubkey;
QList<InviteAddr> addrs;
};
QString encodeInviteLink(const QString& channelId, const QByteArray& pubkey,
const QList<NodeAddr>& addrs);
InviteData decodeInviteLink(const QString& link);

47
tools/chatgui/src/invitedialog.cpp

@ -0,0 +1,47 @@
#include "invitedialog.h"
#include "qrcode_utils.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QClipboard>
#include <QApplication>
InviteDialog::InviteDialog(const QString& link, QWidget* parent)
: QDialog(parent), m_link(link)
{
setWindowTitle("Пригласительная ссылка");
setMinimumWidth(420);
setModal(true);
auto *layout = new QVBoxLayout(this);
layout->setSpacing(12);
auto *linkEdit = new QLineEdit(m_link, this);
linkEdit->setReadOnly(true);
linkEdit->setStyleSheet("padding: 6px; font-family: monospace;");
layout->addWidget(linkEdit);
auto *btnLayout = new QHBoxLayout();
auto *copyBtn = new QPushButton("Скопировать", this);
connect(copyBtn, &QPushButton::clicked, this, [this, linkEdit]() {
QApplication::clipboard()->setText(m_link);
linkEdit->setFocus();
});
btnLayout->addWidget(copyBtn);
btnLayout->addStretch();
layout->addLayout(btnLayout);
auto *qrLabel = new QLabel(this);
QImage qrImage = generateQRCode(m_link);
if (!qrImage.isNull())
qrLabel->setPixmap(QPixmap::fromImage(qrImage).scaled(
180, 180, Qt::KeepAspectRatio, Qt::SmoothTransformation));
qrLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(qrLabel, 0, Qt::AlignCenter);
auto *closeBtn = new QPushButton("OK", this);
connect(closeBtn, &QPushButton::clicked, this, &QDialog::accept);
layout->addWidget(closeBtn, 0, Qt::AlignRight);
}

13
tools/chatgui/src/invitedialog.h

@ -0,0 +1,13 @@
#pragma once
#include <QDialog>
#include <QString>
class InviteDialog : public QDialog {
Q_OBJECT
public:
explicit InviteDialog(const QString& link, QWidget* parent = nullptr);
private:
QString m_link;
};

22
tools/chatgui/src/main.cpp

@ -1,12 +1,32 @@
#include <QApplication>
#include <QFile>
#include <QDir>
#include <QSettings>
#include "mainwindow.h"
#include "../db/db_manager.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
app.setApplicationName("Chat");
app.setQuitOnLastWindowClosed(false);
MainWindow w;
QString cfgPath = QCoreApplication::applicationDirPath() + "/chatgui.cfg";
QString dbPath = "chats.db";
if (QFile::exists(cfgPath)) {
QSettings cfg(cfgPath, QSettings::IniFormat);
dbPath = cfg.value("db_path", "chats.db").toString();
} else {
QSettings cfg(cfgPath, QSettings::IniFormat);
cfg.setValue("db_path", "chats.db");
cfg.sync();
}
if (!QDir::isAbsolutePath(dbPath))
dbPath = QCoreApplication::applicationDirPath() + "/" + dbPath;
DbManager db(dbPath);
MainWindow w(nullptr, &db);
w.show();
return app.exec();

67
tools/chatgui/src/mainwindow.cpp

@ -2,32 +2,66 @@
#include "channellist.h"
#include "messagelist.h"
#include "accountlist.h"
#include "invitedialog.h"
#include "invite_link.h"
#include "../db/db_manager.h"
#include "../transport/msg_client.h"
#include "../transport/chat_propagator.h"
#include "../transport/crypto.h"
#include <QSplitter>
#include <QCloseEvent>
#include <QApplication>
#include <QStyle>
#include <QMessageBox>
MainWindow::MainWindow(QWidget *parent)
MainWindow::MainWindow(QWidget *parent, DbManager* db)
: QMainWindow(parent)
, m_channelList(nullptr)
, m_messageList(nullptr)
, m_accountList(nullptr)
, m_trayIcon(nullptr)
, m_trayMenu(nullptr)
, m_db(db)
, m_client(nullptr)
, m_propagator(nullptr)
{
setWindowTitle("Chat");
resize(900, 600);
setupUi();
setupTray();
if (m_db && m_db->isOpen()) {
m_client = new MsgClient(this);
m_propagator = new ChatPropagator(m_client, m_db, this);
m_messageList->setPropagator(m_propagator);
connect(m_propagator, &ChatPropagator::messageReceived,
m_messageList, &MessageList::addMessage);
connect(m_channelList, &ChannelList::channelSelected,
this, [this](const QString& channelId) {
m_db->setUiState("last_channel_id", channelId);
m_messageList->loadChannel(channelId);
});
connect(m_channelList, &ChannelList::inviteRequested,
this, &MainWindow::showInviteDialog);
m_channelList->loadChannels();
m_accountList->loadAccounts();
QString lastCid = m_db->getUiState("last_channel_id");
if (!lastCid.isEmpty())
m_channelList->selectChannel(lastCid);
}
}
void MainWindow::setupUi() {
auto *splitter = new QSplitter(Qt::Horizontal, this);
m_channelList = new ChannelList(splitter);
m_messageList = new MessageList(splitter);
m_accountList = new AccountList(splitter);
m_channelList = new ChannelList(m_db, splitter);
m_messageList = new MessageList(m_db, splitter);
m_accountList = new AccountList(m_db, splitter);
splitter->addWidget(m_channelList);
splitter->addWidget(m_messageList);
@ -69,7 +103,7 @@ void MainWindow::setupTray() {
void MainWindow::closeEvent(QCloseEvent *event) {
hide();
m_trayIcon->showMessage("Chat", "Application minimized to tray.",
QSystemTrayIcon::Information, 2000);
QSystemTrayIcon::Information, 2000);
event->ignore();
}
@ -81,3 +115,24 @@ void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) {
activateWindow();
}
}
void MainWindow::showInviteDialog(const QString& channelId) {
if (!m_db || !m_db->isOpen()) return;
QByteArray pubkey = m_db->getLocalPubkey();
if (pubkey.size() != 32) {
QMessageBox::warning(this, "Ошибка", "Не удалось получить публичный ключ узла.");
return;
}
QList<NodeAddr> addrs = m_db->getOnlinePeerAddresses(m_db->myNodeId());
QString link = encodeInviteLink(channelId, pubkey, addrs);
if (link.isEmpty()) {
QMessageBox::warning(this, "Ошибка", "Не удалось сформировать пригласительную ссылку.");
return;
}
InviteDialog dlg(link, this);
dlg.exec();
}

15
tools/chatgui/src/mainwindow.h

@ -7,11 +7,14 @@
class ChannelList;
class MessageList;
class AccountList;
class DbManager;
class MsgClient;
class ChatPropagator;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
explicit MainWindow(QWidget *parent = nullptr, DbManager* db = nullptr);
~MainWindow() override = default;
protected:
@ -19,14 +22,18 @@ protected:
private slots:
void onTrayActivated(QSystemTrayIcon::ActivationReason reason);
void showInviteDialog(const QString& channelId);
private:
void setupUi();
void setupTray();
ChannelList *m_channelList;
MessageList *m_messageList;
AccountList *m_accountList;
ChannelList *m_channelList;
MessageList *m_messageList;
AccountList *m_accountList;
QSystemTrayIcon *m_trayIcon;
QMenu *m_trayMenu;
DbManager *m_db;
MsgClient *m_client;
ChatPropagator *m_propagator;
};

187
tools/chatgui/src/messagelist.cpp

@ -6,6 +6,8 @@
#include "emoji.h"
#include "animtimer.h"
#include "lottieicon.h"
#include "../db/db_manager.h"
#include "../transport/chat_propagator.h"
#include <QVBoxLayout>
#include <QPainter>
#include <QPixmap>
@ -61,12 +63,13 @@ static void setMsg(QStandardItem *item,
item->setData(reactions, MsgReactionsRole);
}
MessageList::MessageList(QWidget *parent)
MessageList::MessageList(DbManager* db, QWidget *parent)
: QWidget(parent)
, m_view(new ChatView(this))
, m_model(new QStandardItemModel(this))
, m_inputBar(nullptr)
, m_emojiPanel(nullptr)
, m_db(db)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@ -107,19 +110,26 @@ MessageList::MessageList(QWidget *parent)
layout->addWidget(m_inputBar);
connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) {
QColor youColor(74, 144, 226);
auto av = makeAvatar(youColor, "Y");
auto av = makeAvatar(QColor(74, 144, 226), "Y");
auto *item = new QStandardItem();
auto ts = QDateTime::currentMSecsSinceEpoch();
setMsg(item,
QString::fromUtf8("You"), text,
QDateTime::currentDateTime().toString("HH:mm"),
av, false, false, QString(), QString(),
QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8B"), 1)});
QVariantList{});
m_model->appendRow(item);
m_view->scrollToBottom();
});
populateStubData();
if (m_db && !m_currentChannelId.isEmpty())
m_db->insertMessage(m_currentChannelId, m_db->myNodeId(), text, ts, 1);
if (m_propagator && !m_currentChannelId.isEmpty()) {
static const unsigned char zeroSig[64] = {};
m_propagator->propagateNewMessage(m_currentChannelId, text.toUtf8(), ts,
QByteArray(reinterpret_cast<const char*>(zeroSig), 64));
}
});
connect(AnimTimer::instance(), &AnimTimer::ticked, this, [this]() {
m_view->viewport()->update();
@ -146,107 +156,70 @@ MessageList::MessageList(QWidget *parent)
else if (wasActive && !m_animHoverActive) AnimTimer::instance()->deactivate();
});
}
void MessageList::populateStubData() {
QColor cAlice(74, 144, 226), cBob(126, 211, 33), cCarol(245, 166, 35);
QColor cDave(144, 19, 254), cEve(0, 184, 148);
auto avAlice = makeAvatar(cAlice, "A");
auto avBob = makeAvatar(cBob, "B");
auto avCarol = makeAvatar(cCarol, "C");
auto avDave = makeAvatar(cDave, "D");
auto *m0 = new QStandardItem();
setMsg(m0,
QString::fromUtf8("Alice"),
QString::fromUtf8("Hey team! How's the deployment going?"),
"10:15", avAlice, false,
false, QString(), QString(),
{});
m_model->appendRow(m0);
auto *m1 = new QStandardItem();
setMsg(m1,
QString::fromUtf8("Bob"),
QString::fromUtf8("All green on staging. Running the final integration tests now."),
"10:17", avBob, false,
false, QString(), QString(),
QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 2,
{cAlice, cCarol}),
makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8B"), 3)});
m_model->appendRow(m1);
auto *m2 = new QStandardItem();
setMsg(m2,
QString::fromUtf8("Carol"),
QString::fromUtf8("I've pushed the ETCP-42 fix to staging. BGP handshake should be stable now. Please review the patch when you get a chance."),
"10:22", avCarol, true,
false, QString(), QString(),
QVariantList{
makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 3,
{cAlice, cBob, cDave}),
makeReaction(QString::fromUtf8("\xE2\x9D\xA4\xEF\xB8\x8F"), 1,
{cAlice})
});
m_model->appendRow(m2);
auto *m3 = new QStandardItem();
setMsg(m3,
QString::fromUtf8("Dave"),
QString::fromUtf8("Wait — the BGP session is still flapping after the handshake. I'm seeing the keepalive timer fire but no route updates come through. Logs attached."),
"10:25", avDave, false,
true,
QString::fromUtf8("Carol"),
QString::fromUtf8("BGP handshake should be stable now"),
QVariantList{
makeReaction(QString::fromUtf8("\xF0\x9F\x98\xAE"), 1,
{cBob})
});
m_model->appendRow(m3);
auto *m4 = new QStandardItem();
setMsg(m4,
QString::fromUtf8("Alice"),
QString::fromUtf8("Reviewed the patch. LGTM. Let's merge and deploy to production tomorrow morning."),
"10:28", avAlice, false,
false, QString(), QString(),
QVariantList{
makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 5),
makeReaction(QString::fromUtf8("\xF0\x9F\x8E\x89"), 2,
{cBob, cEve})
});
m_model->appendRow(m4);
auto *m5 = new QStandardItem();
setMsg(m5,
QString::fromUtf8("Bob"),
QString::fromUtf8("Can you share the full logs for the BGP session? I'll dig into the keepalive timer logic."),
"10:30", avBob, false,
true,
QString::fromUtf8("Dave"),
QString::fromUtf8("keepalive timer fire but no route updates"),
{});
m_model->appendRow(m5);
auto *m6 = new QStandardItem();
setMsg(m6,
QString::fromUtf8("Carol"),
QString::fromUtf8("Sounds good \xF0\x9F\x91\x8B. Let's coordinate in #dev-team. I'll prepare the release notes."),
"10:33", avCarol, false,
false, QString(), QString(),
QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\xA4\x96"), 2)});
m_model->appendRow(m6);
auto *m7 = new QStandardItem();
setMsg(m7,
QString::fromUtf8("Alice"),
QString::fromUtf8("Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main."),
"10:35", avAlice, false,
false, QString(), QString(),
QVariantList{
makeReaction(QString::fromUtf8("\xF0\x9F\x9A\x80"), 3,
{cBob, cCarol, cDave}),
makeReaction(QString::fromUtf8("\xE2\x9D\xA4\xEF\xB8\x8F"), 7)
});
m_model->appendRow(m7);
void MessageList::loadChannel(const QString& channelId) {
m_model->clear();
m_avatarCache.clear();
m_currentChannelId = channelId;
if (!m_db || !m_db->isOpen()) return;
auto msgs = m_db->getMessages(channelId, 100);
struct ReactionKey { qint64 msgId; quint64 node; QString emoji; };
QHash<qint64, QHash<QString, QList<QColor>>> reactMap;
for (const auto& m : msgs) {
AccountRow acc = m_db->getAccount(m.authorNodeId);
QString author = acc.displayName.isEmpty()
? QString("node_%1").arg(m.authorNodeId) : acc.displayName;
QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
QColor color(acc.avatarColor);
if (!m_avatarCache.contains(m.authorNodeId)) {
m_avatarCache[m.authorNodeId] = color;
}
auto avatar = makeAvatar(color, letter);
auto time = QDateTime::fromMSecsSinceEpoch(m.timestamp).toString("HH:mm");
QVariantList reacts;
auto *item = new QStandardItem();
setMsg(item, author, m.content, time, avatar, false,
false, QString(), QString(), reacts);
m_model->appendRow(item);
}
if (m_model->rowCount() > 0)
m_view->scrollToBottom();
m_inputBar->setFocus();
}
void MessageList::addMessage(const QString& channelId, quint64 authorNodeId,
const QByteArray& content, qint64 timestamp) {
if (channelId != m_currentChannelId) return;
AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow();
QString author = acc.displayName.isEmpty()
? QString("node_%1").arg(authorNodeId) : acc.displayName;
QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
QColor color(acc.avatarColor);
auto avatar = makeAvatar(color, letter);
auto time = QDateTime::fromMSecsSinceEpoch(timestamp).toString("HH:mm");
/* dedup */
for (int r = 0; r < m_model->rowCount(); r++) {
auto* it = m_model->item(r);
if (it->data(MsgTimeRole).toString() == time
&& it->data(MsgAuthorRole).toString() == author) return;
}
auto *item = new QStandardItem();
setMsg(item, author, QString::fromUtf8(content), time, avatar, false,
false, QString(), QString(), QVariantList{});
m_model->appendRow(item);
if (m_model->rowCount() > 50)
m_model->removeRow(0);
m_view->scrollToBottom();
}

23
tools/chatgui/src/messagelist.h

@ -2,21 +2,32 @@
#include <QWidget>
#include <QStandardItemModel>
#include <QHash>
class ChatView;
class InputBar;
class EmojiPanel;
class DbManager;
class ChatPropagator;
class MessageList : public QWidget {
Q_OBJECT
public:
explicit MessageList(QWidget *parent = nullptr);
explicit MessageList(DbManager* db = nullptr, QWidget *parent = nullptr);
void loadChannel(const QString& channelId);
void addMessage(const QString& channelId, quint64 authorNodeId,
const QByteArray& content, qint64 timestamp);
void setPropagator(ChatPropagator* p) { m_propagator = p; }
private:
ChatView *m_view;
ChatView *m_view;
QStandardItemModel *m_model;
InputBar *m_inputBar;
EmojiPanel *m_emojiPanel;
bool m_animHoverActive = false;
void populateStubData();
InputBar *m_inputBar;
EmojiPanel *m_emojiPanel;
DbManager *m_db;
ChatPropagator *m_propagator = nullptr;
QString m_currentChannelId;
bool m_animHoverActive = false;
QHash<quint64, QColor> m_avatarCache;
};

26
tools/chatgui/src/qrcode_utils.cpp

@ -0,0 +1,26 @@
#include "qrcode_utils.h"
#include "ZXingQt.h"
QImage generateQRCode(const QString& text, int moduleSize)
{
using namespace ZXingQt;
auto barcode = Barcode::fromText(text, BarcodeFormat::QRCode);
if (!barcode.isValid())
return {};
WriterOptions opts;
if (moduleSize > 0)
opts.scale(moduleSize);
return barcode.toImage(opts);
}
QStringList readQRCodes(const QImage& image)
{
using namespace ZXingQt;
auto results = ReadBarcodes(image);
QStringList texts;
for (const auto& b : results) {
if (b.isValid())
texts.append(b.text());
}
return texts;
}

9
tools/chatgui/src/qrcode_utils.h

@ -0,0 +1,9 @@
#pragma once
#include <QImage>
#include <QString>
#include <QStringList>
QImage generateQRCode(const QString& text, int moduleSize = 8);
QStringList readQRCodes(const QImage& image);

247
tools/chatgui/tests/test_crypto.cpp

@ -0,0 +1,247 @@
/*
* test_crypto.cpp — тесты криптографических функций (Crypto::)
*/
#include <QTest>
#include <QTemporaryFile>
#include <QByteArray>
#include <QElapsedTimer>
#include <QAtomicInt>
#include <QDebug>
#include <cstring>
#include "../transport/crypto.h"
class TestCrypto : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void test_derive_key_sizes();
void test_derive_priv_not_empty();
void test_derive_pub_not_empty();
void test_priv_ne_pub();
void test_sign_verify_roundtrip();
void test_sign_verify_empty();
void test_verify_wrong_sig();
void test_verify_wrong_key();
void test_sign_empty_key();
void test_sign_invalid_key_size();
void test_signFile_verifyFile();
void test_signFile_nonexistent();
void test_sign_verify_100mb_parallel();
private:
void writeTestFile(const QString& path, unsigned char pattern, size_t fileSize);
private:
QByteArray m_x25519Priv; // 32 random bytes
QByteArray m_edPriv; // derived Ed25519 private key
QByteArray m_edPub; // derived Ed25519 public key
QByteArray m_message;
};
void TestCrypto::initTestCase() {
m_x25519Priv = QByteArray(Crypto::kKeySize, 0);
for (int i = 0; i < static_cast<int>(Crypto::kKeySize); i++)
m_x25519Priv[i] = static_cast<char>(0x42 + i); /* детерминированное значение */
m_edPriv = Crypto::deriveEd25519Privkey(m_x25519Priv);
m_edPub = Crypto::deriveEd25519Pubkey(m_x25519Priv);
m_message = QByteArray("hello world");
}
void TestCrypto::test_derive_key_sizes() {
QCOMPARE(m_edPriv.size(), static_cast<int>(Crypto::kKeySize));
QCOMPARE(m_edPub.size(), static_cast<int>(Crypto::kKeySize));
}
void TestCrypto::test_derive_priv_not_empty() {
QVERIFY(!m_edPriv.isEmpty());
}
void TestCrypto::test_derive_pub_not_empty() {
QVERIFY(!m_edPub.isEmpty());
}
void TestCrypto::test_priv_ne_pub() {
QVERIFY(m_edPriv != m_edPub);
}
void TestCrypto::test_sign_verify_roundtrip() {
QByteArray sig = Crypto::sign(m_edPriv, m_message);
QCOMPARE(sig.size(), static_cast<int>(Crypto::kSignSize));
QVERIFY(Crypto::verify(m_edPub, m_message, sig));
}
void TestCrypto::test_sign_verify_empty() {
QByteArray empty;
QByteArray sig = Crypto::sign(m_edPriv, empty);
QCOMPARE(sig.size(), static_cast<int>(Crypto::kSignSize));
QVERIFY(Crypto::verify(m_edPub, empty, sig));
}
void TestCrypto::test_verify_wrong_sig() {
QByteArray sig = Crypto::sign(m_edPriv, m_message);
QVERIFY(!sig.isEmpty());
sig[8] = static_cast<char>(sig[8] ^ 1); /* портим подпись */
QVERIFY(!Crypto::verify(m_edPub, m_message, sig));
}
void TestCrypto::test_verify_wrong_key() {
QByteArray sig = Crypto::sign(m_edPriv, m_message);
/* другой ключ */
QByteArray otherPriv(Crypto::kKeySize, 0);
for (int i = 0; i < static_cast<int>(Crypto::kKeySize); i++)
otherPriv[i] = static_cast<char>(0x77 + i);
QByteArray otherPub = Crypto::deriveEd25519Pubkey(otherPriv);
QVERIFY(!otherPub.isEmpty());
QVERIFY(!Crypto::verify(otherPub, m_message, sig));
}
void TestCrypto::test_sign_empty_key() {
QByteArray sig = Crypto::sign(QByteArray(), m_message);
QVERIFY(sig.isEmpty());
QVERIFY(!Crypto::verify(QByteArray(), m_message, sig));
}
void TestCrypto::test_sign_invalid_key_size() {
QByteArray badKey(16, 0x42);
QByteArray sig = Crypto::sign(badKey, m_message);
QVERIFY(sig.isEmpty());
}
void TestCrypto::test_signFile_verifyFile() {
QTemporaryFile tmp;
QVERIFY(tmp.open());
QByteArray content("file content for signing test");
tmp.write(content);
tmp.close();
QByteArray sig;
QEventLoop loop;
Crypto::signFile(tmp.fileName(), m_edPriv,
[&](const Crypto::SignResult& r) {
sig = r.signature;
QVERIFY(r.ok);
QVERIFY(!sig.isEmpty());
loop.quit();
});
loop.exec();
QEventLoop loop2;
Crypto::verifyFile(tmp.fileName(), m_edPub, sig,
[&](const Crypto::SignResult& r) {
QVERIFY(r.ok);
loop2.quit();
});
loop2.exec();
}
void TestCrypto::test_signFile_nonexistent() {
QEventLoop loop;
Crypto::signFile("/nonexistent/path/test", m_edPriv,
[&](const Crypto::SignResult& r) {
QVERIFY(!r.ok);
QVERIFY(!r.error.isEmpty());
loop.quit();
});
loop.exec();
}
void TestCrypto::writeTestFile(const QString& path, unsigned char pattern, size_t fileSize) {
QFile f(path);
QVERIFY(f.open(QIODevice::WriteOnly));
constexpr size_t kChunkSize = 65536;
QByteArray chunk(static_cast<int>(kChunkSize), Qt::Uninitialized);
memset(chunk.data(), pattern, kChunkSize);
size_t remaining = fileSize;
while (remaining >= kChunkSize) {
QCOMPARE(f.write(chunk), static_cast<qint64>(kChunkSize));
remaining -= kChunkSize;
}
if (remaining > 0) f.write(chunk.constData(), static_cast<qint64>(remaining));
f.close();
}
void TestCrypto::test_sign_verify_100mb_parallel() {
constexpr size_t kFileSize = 100ULL * 1024 * 1024;
constexpr int kNumFiles = 3;
QString paths[kNumFiles];
QTemporaryFile tmp[kNumFiles];
for (int i = 0; i < kNumFiles; i++) {
QVERIFY(tmp[i].open());
paths[i] = tmp[i].fileName();
tmp[i].close();
writeTestFile(paths[i], static_cast<unsigned char>(0xAA + i), kFileSize);
}
/* ------- Sign (параллельно) ------- */
QByteArray sigs[kNumFiles];
qint64 signTimesMs[kNumFiles] = {};
QElapsedTimer totalSignTimer;
QAtomicInt signDone{0};
QEventLoop signLoop;
totalSignTimer.start();
for (int i = 0; i < kNumFiles; i++) {
QElapsedTimer t;
t.start();
Crypto::signFile(paths[i], m_edPriv,
[&, i, t](const Crypto::SignResult& r) mutable {
QVERIFY(r.ok);
QVERIFY(!r.signature.isEmpty());
sigs[i] = r.signature;
signTimesMs[i] = t.elapsed();
if (signDone.fetchAndAddRelaxed(1) == kNumFiles - 1)
signLoop.quit();
});
}
signLoop.exec();
qint64 totalSignMs = totalSignTimer.elapsed();
/* ------- Verify (параллельно) ------- */
qint64 verifyTimesMs[kNumFiles] = {};
QElapsedTimer totalVerifyTimer;
QAtomicInt verifyDone{0};
QEventLoop verifyLoop;
totalVerifyTimer.start();
for (int i = 0; i < kNumFiles; i++) {
QElapsedTimer t;
t.start();
Crypto::verifyFile(paths[i], m_edPub, sigs[i],
[&, i, t](const Crypto::SignResult& r) mutable {
QVERIFY(r.ok);
verifyTimesMs[i] = t.elapsed();
if (verifyDone.fetchAndAddRelaxed(1) == kNumFiles - 1)
verifyLoop.quit();
});
}
verifyLoop.exec();
qint64 totalVerifyMs = totalVerifyTimer.elapsed();
/* ------- Профилирование ------- */
qint64 signSum = 0, verifySum = 0;
for (int i = 0; i < kNumFiles; i++) {
signSum += signTimesMs[i];
verifySum += verifyTimesMs[i];
}
qDebug() << "\n======= test_sign_verify_100mb_parallel =======";
for (int i = 0; i < kNumFiles; i++)
qDebug().nospace() << " File " << i << ": sign=" << signTimesMs[i]
<< "ms | verify=" << verifyTimesMs[i] << "ms";
qDebug().nospace() << "Phase sign: " << totalSignMs << "ms total"
<< " (sum=" << signSum << "ms, speedup="
<< (double)signSum / totalSignMs << "x)";
qDebug().nospace() << "Phase verify: " << totalVerifyMs << "ms total"
<< " (sum=" << verifySum << "ms, speedup="
<< (double)verifySum / totalVerifyMs << "x)";
/* Вычищаем временные файлы */
for (int i = 0; i < kNumFiles; i++) QFile::remove(paths[i]);
}
QTEST_MAIN(TestCrypto)
#include "test_crypto.moc"

376
tools/chatgui/transport/chat_propagator.cpp

@ -0,0 +1,376 @@
#include "chat_propagator.h"
#include "chat_proto.h"
#include "msg_client.h"
#include "crypto.h"
#include "db_manager.h"
#include <QDateTime>
#include <algorithm>
#include <random>
static uint64_t read64(const uint8_t* p) {
uint64_t v; memcpy(&v, p, 8); return v;
}
static void write64(uint8_t* p, uint64_t v) { memcpy(p, &v, 8); }
static uint32_t read32(const uint8_t* p) {
uint32_t v; memcpy(&v, p, 4); return v;
}
static void write32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); }
static uint16_t read16(const uint8_t* p) {
uint16_t v; memcpy(&v, p, 2); return v;
}
static void write16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); }
ChatPropagator::ChatPropagator(MsgClient* client, DbManager* db, QObject* parent)
: QObject(parent)
, m_client(client)
, m_db(db)
, m_ackTimer(new QTimer(this))
{
connect(m_client, &MsgClient::received,
this, &ChatPropagator::onReceived);
connect(m_ackTimer, &QTimer::timeout,
this, &ChatPropagator::onAckTimeout);
}
void ChatPropagator::setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId) {
m_edPriv = edPriv;
m_edPub = edPub;
m_myNodeId = nodeId;
}
void ChatPropagator::start() {
if (!m_edPriv.isEmpty() && !m_ackTimer->isActive())
m_ackTimer->start(250);
startCatchUp();
}
/* ======================================================================== */
void ChatPropagator::propagateNewMessage(const QString& channelId,
const QByteArray& content,
qint64 timestamp,
const QByteArray& signature) {
if (m_edPriv.isEmpty() || !m_myNodeId) return;
QByteArray packet = buildNewMsgPacket(channelId, content, timestamp, signature);
QList<quint64> exclude;
exclude.append(m_myNodeId);
fanoutGossip(channelId, packet, signature, ChatProp::kDefaultFanout, exclude);
}
QByteArray ChatPropagator::buildNewMsgPacket(const QString& channelId,
const QByteArray& content,
qint64 timestamp,
const QByteArray& signature) {
QByteArray cid = channelId.toUtf8();
uint8_t cidLen = static_cast<uint8_t>(cid.size());
size_t total = 1 + 1 + cidLen + 8 + 2 + content.size() + ChatProp::kSignSize;
QByteArray pkt(static_cast<int>(total), Qt::Uninitialized);
uint8_t* p = reinterpret_cast<uint8_t*>(pkt.data());
*p++ = ChatProp::kNewMsg;
*p++ = cidLen;
memcpy(p, cid.constData(), cidLen); p += cidLen;
write64(p, static_cast<uint64_t>(timestamp)); p += 8;
write16(p, static_cast<uint16_t>(content.size())); p += 2;
memcpy(p, content.constData(), content.size()); p += content.size();
memcpy(p, signature.constData(), ChatProp::kSignSize);
return pkt;
}
QByteArray ChatPropagator::buildAckPacket(const QByteArray& sig) {
QByteArray pkt(static_cast<int>(1 + ChatProp::kSignSize), Qt::Uninitialized);
pkt[0] = static_cast<char>(ChatProp::kNewAck);
memcpy(pkt.data() + 1, sig.constData(), ChatProp::kSignSize);
return pkt;
}
/* ======================================================================== */
void ChatPropagator::fanoutGossip(const QString& channelId, const QByteArray& packet,
const QByteArray& sig, int fanout,
const QList<quint64>& exclude) {
auto peers = pickRandomPeers(fanout, exclude);
if (peers.isEmpty()) return;
GossipEntry e;
e.channelId = channelId;
e.packet = packet;
e.sig = sig;
e.peers = peers;
e.sentAt = QDateTime::currentMSecsSinceEpoch();
m_pending[sig] = e;
for (auto peer : peers)
sendGossip(peer, packet);
}
void ChatPropagator::sendGossip(quint64 peer, const QByteArray& packet) {
m_client->sendTo(peer, packet);
}
/* ======================================================================== */
void ChatPropagator::onReceived(quint64 peerId, const QByteArray& data) {
if (data.isEmpty()) return;
uint8_t type = static_cast<uint8_t>(data[0]);
switch (type) {
case ChatProp::kNewMsg: handleNewMsg(peerId, data); break;
case ChatProp::kNewAck: handleNewAck(data); break;
case ChatProp::kBucketsRsp: handleBucketsRsp(data); break;
case ChatProp::kHashesRsp: handleHashesRsp(data); break;
case ChatProp::kMsgsRsp: handleMsgsRsp(data); break;
}
}
void ChatPropagator::handleNewMsg(quint64 fromPeer, const QByteArray& payload) {
QString channelId;
QByteArray content;
qint64 timestamp;
QByteArray sig;
if (!parseNewMsg(payload, channelId, content, timestamp, sig))
return;
/* verify sender's signature */
QByteArray peerEdPub = m_db->getNodeEdPub(fromPeer);
if (!peerEdPub.isEmpty() && !Crypto::verify(peerEdPub, payload.left(payload.size() - 64), sig))
return;
/* insert into DB */
m_db->insertMessage(channelId, fromPeer, content, timestamp, 0);
emit messageReceived(channelId, fromPeer, content, timestamp);
/* re-gossip */
QList<quint64> exclude;
exclude.append(m_myNodeId);
exclude.append(fromPeer);
fanoutGossip(channelId, payload, sig, ChatProp::kDefaultFanout, exclude);
/* ack to sender */
m_client->sendTo(fromPeer, buildAckPacket(sig));
}
void ChatPropagator::handleNewAck(const QByteArray& payload) {
if (payload.size() < static_cast<int>(1 + ChatProp::kSignSize)) return;
QByteArray sig = payload.mid(1, static_cast<int>(ChatProp::kSignSize));
auto it = m_pending.find(sig);
if (it == m_pending.end()) return;
/* mark all peers as acked (simple version: ACK means message delivered) */
m_pending.erase(it);
}
void ChatPropagator::onAckTimeout() {
qint64 now = QDateTime::currentMSecsSinceEpoch();
QList<QByteArray> toRemove;
for (auto it = m_pending.begin(); it != m_pending.end(); ++it) {
auto& e = it.value();
if (now - e.sentAt < ChatProp::kDefaultAckTimeoutMs) continue;
if (e.retries >= 3) {
toRemove.append(it.key());
continue;
}
/* retry: pick new peers for each missing ACK */
QList<quint64> unacked;
for (auto p : e.peers)
if (!e.acked.contains(p)) unacked.append(p);
if (!unacked.isEmpty()) {
auto newPeers = pickRandomPeers(1, e.peers + e.acked +
QList<quint64>{m_myNodeId});
if (!newPeers.isEmpty()) {
sendGossip(newPeers.first(), e.packet);
e.peers.append(newPeers.first());
}
}
e.sentAt = now;
e.retries++;
}
for (auto& k : toRemove) m_pending.remove(k);
}
/* ========================================================================
* Catch-up
* ======================================================================== */
void ChatPropagator::startCatchUp() {
if (!m_db) return;
auto channels = m_db->getChannels();
if (channels.isEmpty()) return;
auto peers = pickRandomPeers(ChatProp::kDefaultFanout, {m_myNodeId});
if (peers.isEmpty()) return;
for (auto& ch : channels) {
QByteArray cid = ch.channelId.toUtf8();
QByteArray pkt(static_cast<int>(2 + cid.size()), Qt::Uninitialized);
pkt[0] = static_cast<char>(ChatProp::kBucketsReq);
pkt[1] = static_cast<char>(cid.size());
memcpy(pkt.data() + 2, cid.constData(), cid.size());
for (auto peer : peers)
sendGossip(peer, pkt);
}
}
void ChatPropagator::handleBucketsRsp(const QByteArray& payload) {
if (payload.size() < 3) return;
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData());
if (*p != ChatProp::kBucketsRsp) return; p++;
uint8_t cidLen = *p++;
if (p + cidLen + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) return;
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen;
uint16_t bCount = read16(p); p += 2;
for (int i = 0; i < bCount; i++) {
if (p + 4 + 1 + 4 + 32 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break;
qint64 day = static_cast<qint64>(read32(p)); p += 4;
int hour = *p++;
int rCount = static_cast<int>(read32(p)); p += 4;
QByteArray rHash(reinterpret_cast<const char*>(p), 32); p += 32;
auto local = m_db->getBucketMeta(channelId);
bool found = false;
for (auto& b : local) {
if (b.day == day && b.hour == hour && b.count == rCount && b.hash == rHash) {
found = true; break;
}
}
if (found) continue;
/* mismatch — request signatures */
QByteArray cid = channelId.toUtf8();
QByteArray req(static_cast<int>(7 + cid.size()), Qt::Uninitialized);
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data());
*rp++ = ChatProp::kHashesReq;
*rp++ = static_cast<uint8_t>(cid.size());
memcpy(rp, cid.constData(), cid.size()); rp += cid.size();
write32(rp, static_cast<uint32_t>(day)); rp += 4;
*rp = static_cast<uint8_t>(hour);
auto peers = pickRandomPeers(1, {m_myNodeId});
if (!peers.isEmpty())
sendGossip(peers.first(), req);
}
}
void ChatPropagator::handleHashesRsp(const QByteArray& payload) {
if (payload.size() < 3) return;
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData());
if (*p != ChatProp::kHashesRsp) return; p++;
uint16_t sigCount = read16(p); p += 2;
QList<QByteArray> remoteSigs;
for (int i = 0; i < sigCount; i++) {
if (p + 64 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break;
remoteSigs.append(QByteArray(reinterpret_cast<const char*>(p), 64));
p += 64;
}
if (remoteSigs.isEmpty()) return;
/* find missing */
QList<QByteArray> missing;
QList<QByteArray> remote = remoteSigs;
for (auto& s : remote) {
/* TODO: efficient lookup — query DB for this sig */
auto msgs = m_db->getMessagesBySigs({s});
if (msgs.isEmpty()) missing.append(s);
}
if (missing.isEmpty()) return;
/* request missing messages */
int mCount = missing.size();
QByteArray req(static_cast<int>(3 + mCount * 64), Qt::Uninitialized);
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data());
*rp++ = ChatProp::kMsgsReq;
write16(rp, static_cast<uint16_t>(mCount)); rp += 2;
for (auto& s : missing) { memcpy(rp, s.constData(), 64); rp += 64; }
auto peers = pickRandomPeers(1, {m_myNodeId});
if (!peers.isEmpty())
sendGossip(peers.first(), req);
}
void ChatPropagator::handleMsgsRsp(const QByteArray& payload) {
if (payload.size() < 4) return;
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData());
const uint8_t* end = p + payload.size();
if (*p != ChatProp::kMsgsRsp) return; p++;
uint8_t cidLen = *p++;
if (p + cidLen + 2 > end) return;
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen;
uint16_t msgCount = read16(p); p += 2;
for (int i = 0; i < msgCount; i++) {
if (p + 64 + 8 + 8 + 2 > end) break;
QByteArray sig(reinterpret_cast<const char*>(p), 64); p += 64;
quint64 author = read64(p); p += 8;
qint64 ts = static_cast<qint64>(read64(p)); p += 8;
uint16_t cLen = read16(p); p += 2;
if (p + cLen > end) break;
QByteArray content(reinterpret_cast<const char*>(p), cLen); p += cLen;
auto existing = m_db->getMessagesBySigs({sig});
if (!existing.isEmpty()) continue;
m_db->insertMessage(channelId, author, content, ts, 0);
emit messageReceived(channelId, author, content, ts);
}
}
/* ======================================================================== */
bool ChatPropagator::parseNewMsg(const QByteArray& payload, QString& channelId,
QByteArray& content, qint64& timestamp,
QByteArray& sig) {
if (payload.size() < 14) return false; /* minimum: 1+1+1+8+2+1 */
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData());
if (*p != ChatProp::kNewMsg) return false;
p++;
uint8_t cidLen = *p++;
if (p + cidLen + 8 + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size())
return false;
channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen;
timestamp = static_cast<qint64>(read64(p)); p += 8;
uint16_t contentLen = read16(p); p += 2;
if (p + contentLen + ChatProp::kSignSize >
reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size())
return false;
content = QByteArray(reinterpret_cast<const char*>(p), contentLen); p += contentLen;
sig = QByteArray(reinterpret_cast<const char*>(p), static_cast<int>(ChatProp::kSignSize));
return true;
}
QList<quint64> ChatPropagator::pickRandomPeers(int count, const QList<quint64>& exclude) {
QList<quint64> result;
if (!m_db) return result;
auto accs = m_db->getAccounts(true);
QList<quint64> pool;
for (auto& a : accs) {
if (!exclude.contains(a.nodeId))
pool.append(a.nodeId);
}
if (pool.isEmpty()) return result;
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(pool.begin(), pool.end(), g);
for (int i = 0; i < count && i < pool.size(); i++)
result.append(pool[i]);
return result;
}

70
tools/chatgui/transport/chat_propagator.h

@ -0,0 +1,70 @@
#pragma once
#include <QObject>
#include <QByteArray>
#include <QHash>
#include <QList>
#include <QTimer>
#include <cstdint>
class MsgClient;
class DbManager;
struct MessageRow;
class ChatPropagator : public QObject {
Q_OBJECT
public:
ChatPropagator(MsgClient* client, DbManager* db, QObject* parent = nullptr);
void setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId);
void start();
void propagateNewMessage(const QString& channelId, const QByteArray& content,
qint64 timestamp, const QByteArray& signature);
signals:
void messageReceived(const QString& channelId, quint64 authorNodeId,
const QByteArray& content, qint64 timestamp);
private:
MsgClient* m_client;
DbManager* m_db;
QTimer* m_ackTimer;
QByteArray m_edPriv;
QByteArray m_edPub;
quint64 m_myNodeId = 0;
struct GossipEntry {
QString channelId;
QByteArray packet; /* full NEW_MSG packet */
QByteArray sig; /* message signature (key) */
QList<quint64> peers; /* target peers */
QList<quint64> acked; /* peers that acknowledged */
qint64 sentAt; /* when sent (ms) */
int retries = 0;
};
QHash<QByteArray, GossipEntry> m_pending; /* sig → entry */
QByteArray m_mySig; /* sig we're waiting for ACK on */
void onReceived(quint64 peerId, const QByteArray& data);
void handleNewMsg(quint64 fromPeer, const QByteArray& payload);
void handleNewAck(const QByteArray& payload);
void onAckTimeout();
void startCatchUp();
void handleBucketsRsp(const QByteArray& payload);
void handleHashesRsp(const QByteArray& payload);
void handleMsgsRsp(const QByteArray& payload);
void fanoutGossip(const QString& channelId, const QByteArray& packet,
const QByteArray& sig, int fanout, const QList<quint64>& exclude);
void sendGossip(quint64 peer, const QByteArray& packet);
QList<quint64> pickRandomPeers(int count, const QList<quint64>& exclude);
QByteArray buildNewMsgPacket(const QString& channelId, const QByteArray& content,
qint64 timestamp, const QByteArray& signature);
QByteArray buildAckPacket(const QByteArray& sig);
bool parseNewMsg(const QByteArray& payload, QString& channelId, QByteArray& content,
qint64& timestamp, QByteArray& sig);
};

48
tools/chatgui/transport/chat_proto.h

@ -0,0 +1,48 @@
/*
* chat_proto.h — протокол распространения сообщений чата (gossip + catch-up)
*
* Все пакеты идут через MsgClient::sendTo() / received() поверх msg_transport.
* Подписи — внутри пейлоада. utun — чистый транспорт.
*/
#ifndef CHAT_PROPAGATE_PROTO_H
#define CHAT_PROPAGATE_PROTO_H
#include <cstdint>
namespace ChatProp {
static constexpr uint8_t kNewMsg = 0x01; /* новое сообщение */
static constexpr uint8_t kNewAck = 0x02; /* подтверждение получения */
static constexpr uint8_t kBucketsReq = 0x03; /* запрос bucket-ов канала */
static constexpr uint8_t kBucketsRsp = 0x04; /* bucket-ы: день+час → count+hash */
static constexpr uint8_t kHashesReq = 0x05; /* запрос подписей в bucket */
static constexpr uint8_t kHashesRsp = 0x06; /* список подписей (64 байта каждая) */
static constexpr uint8_t kMsgsReq = 0x07; /* запрос сообщений по подписям */
static constexpr uint8_t kMsgsRsp = 0x08; /* сообщения + подписи */
static constexpr size_t kSignSize = 64;
static constexpr size_t kHashSize = 32; /* SHA-256 */
static constexpr int kDefaultFanout = 4;
static constexpr int kDefaultAckTimeoutMs = 1000;
/* Форматы пакетов (byte stream) ================================================
NEW_MSG: [0x01][channel_id_len:1][channel_id][timestamp:8][content_len:2][content][sig:64]
NEW_ACK: [0x02][sig_of_confirmed_msg:64]
BUCKETS_REQ: [0x03][channel_id_len:1][channel_id]
BUCKETS_RSP: [0x04][channel_id_len:1][channel_id][bucket_count:2][buckets...]
bucket = [day_unix:4][hour:1][msg_count:4][sig_hash:32]
HASHES_REQ: [0x05][channel_id_len:1][channel_id][day_unix:4][hour:1]
HASHES_RSP: [0x06][sig_count:2][sig:64, sig:64, ...]
MSGS_REQ: [0x07][sig_count:2][sig:64, sig:64, ...]
MSGS_RSP: [0x08][channel_id_len:1][channel_id][msg_count:2][(sig:64, author:8, timestamp:8, content_len:2, content)...]
=============================================================================== */
} // namespace ChatProp
#endif

221
tools/chatgui/transport/crypto.cpp

@ -0,0 +1,221 @@
/*
* crypto.cpp — Криптографические утилиты (Ed25519 через OpenSSL)
*/
#include "crypto.h"
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <QFile>
#include <QCoreApplication>
#include <thread>
#include <cstring>
namespace Crypto {
/* ============================================================================
* Помощники
* ============================================================================ */
namespace {
void postToMain(std::function<void()> fn) {
auto* app = QCoreApplication::instance();
if (app) {
QMetaObject::invokeMethod(app, std::move(fn), Qt::QueuedConnection);
} else {
fn(); /* нет event loop — синхронно */
}
}
QByteArray sha512File(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) return {};
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
if (!md_ctx || EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL) != 1) {
if (md_ctx) EVP_MD_CTX_free(md_ctx);
return {};
}
QByteArray buf(65536, 0);
while (true) {
qint64 n = file.read(buf.data(), buf.size());
if (n <= 0) break;
EVP_DigestUpdate(md_ctx, reinterpret_cast<const uint8_t*>(buf.constData()),
static_cast<size_t>(n));
}
file.close();
QByteArray hash;
hash.resize(static_cast<int>(kSha512Size));
unsigned int hash_len = static_cast<unsigned int>(kSha512Size);
if (EVP_DigestFinal_ex(md_ctx, reinterpret_cast<uint8_t*>(hash.data()), &hash_len) != 1)
hash.clear();
EVP_MD_CTX_free(md_ctx);
return hash;
}
} // anonymous
/* ============================================================================
* Синхронные: derive ключей
* ============================================================================ */
QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey) {
if (x25519Privkey.size() != static_cast<int>(kKeySize)) return QByteArray();
uint8_t hash[kSha512Size];
SHA512(reinterpret_cast<const uint8_t*>(x25519Privkey.constData()), kKeySize, hash);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize);
if (!pkey) return QByteArray();
QByteArray result;
result.resize(static_cast<int>(kKeySize));
size_t priv_len = kKeySize;
if (EVP_PKEY_get_raw_private_key(pkey, reinterpret_cast<uint8_t*>(result.data()), &priv_len) <= 0
|| priv_len != kKeySize) {
EVP_PKEY_free(pkey);
return QByteArray();
}
EVP_PKEY_free(pkey);
return result;
}
QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey) {
if (x25519Privkey.size() != static_cast<int>(kKeySize)) return QByteArray();
uint8_t hash[kSha512Size];
SHA512(reinterpret_cast<const uint8_t*>(x25519Privkey.constData()), kKeySize, hash);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize);
if (!pkey) return QByteArray();
QByteArray result;
result.resize(static_cast<int>(kKeySize));
size_t pub_len = kKeySize;
if (EVP_PKEY_get_raw_public_key(pkey, reinterpret_cast<uint8_t*>(result.data()), &pub_len) <= 0
|| pub_len != kKeySize) {
EVP_PKEY_free(pkey);
return QByteArray();
}
EVP_PKEY_free(pkey);
return result;
}
/* ============================================================================
* Синхронные: подпись / проверка (малые блоки)
* ============================================================================ */
QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data) {
if (ed25519Privkey.size() != static_cast<int>(kKeySize)) return QByteArray();
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL,
reinterpret_cast<const uint8_t*>(ed25519Privkey.constData()), kKeySize);
if (!pkey) return QByteArray();
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
if (!md_ctx || EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, pkey) != 1) {
if (md_ctx) EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
return QByteArray();
}
size_t sig_len = kSignSize;
QByteArray result;
result.resize(static_cast<int>(kSignSize));
if (EVP_DigestSign(md_ctx, reinterpret_cast<uint8_t*>(result.data()), &sig_len,
reinterpret_cast<const uint8_t*>(data.constData()), data.size()) != 1) {
EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
return QByteArray();
}
EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
result.resize(static_cast<int>(sig_len));
return result;
}
bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig) {
if (ed25519Pubkey.size() != static_cast<int>(kKeySize)
|| sig.size() > static_cast<int>(kSignSize)) return false;
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
reinterpret_cast<const uint8_t*>(ed25519Pubkey.constData()), kKeySize);
if (!pkey) return false;
EVP_MD_CTX* md_ctx = EVP_MD_CTX_new();
if (!md_ctx || EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, pkey) != 1) {
if (md_ctx) EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
return false;
}
int ret = EVP_DigestVerify(md_ctx, reinterpret_cast<const uint8_t*>(sig.constData()), sig.size(),
reinterpret_cast<const uint8_t*>(data.constData()), data.size());
EVP_MD_CTX_free(md_ctx);
EVP_PKEY_free(pkey);
return ret == 1;
}
/* ============================================================================
* Асинхронные: файлы
* ============================================================================ */
void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb) {
std::thread([=]() {
SignResult r;
QByteArray hash = sha512File(path);
if (hash.isEmpty()) {
r.ok = false; r.error = QStringLiteral("failed to read/hash file");
} else {
r.signature = sign(ed25519Privkey, hash);
r.ok = !r.signature.isEmpty();
if (!r.ok) r.error = QStringLiteral("sign failed");
}
postToMain([cb, r]() { cb(r); });
}).detach();
}
void verifyFile(const QString& path, const QByteArray& ed25519Pubkey,
const QByteArray& signature, SignCallback cb) {
std::thread([=]() {
SignResult r;
QByteArray hash = sha512File(path);
if (hash.isEmpty()) {
r.ok = false; r.error = QStringLiteral("failed to read/hash file");
} else {
r.ok = verify(ed25519Pubkey, hash, signature);
if (!r.ok) r.error = QStringLiteral("signature mismatch");
}
postToMain([cb, r]() { cb(r); });
}).detach();
}
/* ============================================================================
* Асинхронные: in-memory блоки
* ============================================================================ */
void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb) {
std::thread([=]() {
SignResult r;
r.signature = sign(ed25519Privkey, data);
r.ok = !r.signature.isEmpty();
if (!r.ok) r.error = QStringLiteral("sign failed");
postToMain([cb, r]() { cb(r); });
}).detach();
}
void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data,
const QByteArray& signature, SignCallback cb) {
std::thread([=]() {
SignResult r;
r.ok = verify(ed25519Pubkey, data, signature);
if (!r.ok) r.error = QStringLiteral("signature mismatch");
postToMain([cb, r]() { cb(r); });
}).detach();
}
} // namespace Crypto

48
tools/chatgui/transport/crypto.h

@ -0,0 +1,48 @@
/*
* crypto.h — Криптографические утилиты для chat GUI
*
* Ключи: X25519 → SHA-512 → Ed25519 (аналог sc_derive_ed25519_key / pubkey в utun)
* Подпись: Ed25519 (EVP_DigestSign / EVP_DigestVerify)
*/
#ifndef MSG_CRYPTO_H
#define MSG_CRYPTO_H
#include <QByteArray>
#include <QString>
#include <functional>
namespace Crypto {
static constexpr size_t kKeySize = 32; // X25519 / Ed25519 key size
static constexpr size_t kSignSize = 64; // Ed25519 signature size
static constexpr size_t kSha512Size = 64; // SHA-512 digest size
// ---------------- синхронные (малые блоки) ----------------
QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey);
QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey);
QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data);
bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig);
// ---------------- асинхронные (файлы / большие блоки) ----------------
struct SignResult {
bool ok = false;
QByteArray signature; // 64 bytes
QString error;
};
using SignCallback = std::function<void(const SignResult&)>;
void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb);
void verifyFile(const QString& path, const QByteArray& ed25519Pubkey,
const QByteArray& signature, SignCallback cb);
void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb);
void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data,
const QByteArray& signature, SignCallback cb);
} // namespace Crypto
#endif // MSG_CRYPTO_H

31
tools/chatgui/transport/msg_client.cpp

@ -71,6 +71,26 @@ void MsgClient::sendSigned(const QByteArray& data) {
m_socket->write(msg);
}
void MsgClient::sendTo(quint64 dstPeerId, const QByteArray& data) {
QByteArray msg = buildMessage(kDataSend, dstPeerId, 0, data);
m_socket->write(msg);
}
void MsgClient::requestPublicKey() {
QByteArray msg = buildMessage(kCtlGetPubkey, 0, 0, QByteArray());
m_socket->write(msg);
}
void MsgClient::requestPrivateKey() {
QByteArray msg = buildMessage(kCtlGetPrivkey, 0, 0, QByteArray());
m_socket->write(msg);
}
void MsgClient::requestPeerInfo(quint64 peerId) {
QByteArray msg = buildMessage(kCtlGetPeerInfo, peerId, 0, QByteArray());
m_socket->write(msg);
}
/* ============================================================================
* Внутренние
* ============================================================================ */
@ -118,6 +138,17 @@ void MsgClient::dispatch(uint8_t type, quint64 id, const QByteArray& payload) {
m_boundPeerId = id;
emit bound(id);
break;
case kRspPubkey:
emit publicKeyReceived(payload);
break;
case kRspPrivkey:
emit privateKeyReceived(payload);
break;
case kRspPeerInfo:
if (payload.size() >= 64) {
emit peerInfoReceived(id, payload.left(32), payload.mid(32, 32));
}
break;
case kDataRecv:
emit received(id, payload);
break;

15
tools/chatgui/transport/msg_client.h

@ -31,6 +31,11 @@ public:
void send(const QByteArray& data);
void send(const uint8_t* data, size_t len);
void sendSigned(const QByteArray& data);
void sendTo(quint64 dstPeerId, const QByteArray& data);
void requestPublicKey();
void requestPrivateKey();
void requestPeerInfo(quint64 peerId);
signals:
void connected();
@ -39,14 +44,24 @@ signals:
void unbound();
void received(quint64 peerId, const QByteArray& data);
void errorOccurred(uint8_t code, const QString& message);
void publicKeyReceived(const QByteArray& key); // 32 байта X25519
void privateKeyReceived(const QByteArray& key); // 32 байта X25519
void peerInfoReceived(quint64 peerId, const QByteArray& x25519Pubkey,
const QByteArray& ed25519Pubkey);
private:
static constexpr size_t kMaxMsgSize = 8192;
static constexpr uint8_t kCtlBind = 0x01;
static constexpr uint8_t kCtlUnbind = 0x02;
static constexpr uint8_t kCtlGetPubkey = 0x03;
static constexpr uint8_t kCtlGetPrivkey = 0x04;
static constexpr uint8_t kCtlGetPeerInfo = 0x05;
static constexpr uint8_t kDataSend = 0x10;
static constexpr uint8_t kDataRecv = 0x11;
static constexpr uint8_t kRspBindOk = 0xE1;
static constexpr uint8_t kRspPubkey = 0xE2;
static constexpr uint8_t kRspPrivkey = 0xE3;
static constexpr uint8_t kRspPeerInfo = 0xE4;
static constexpr uint8_t kRspError = 0xFF;
static constexpr uint8_t kOptSigned = 0x01;
static constexpr size_t kHdrSize = 12;

43
tools/chatgui/zxing-cpp/.clang-format

@ -0,0 +1,43 @@
---
Language: Cpp
Standard: c++17
BasedOnStyle: LLVM
IndentWidth: 4
TabWidth: 4
UseTab: ForContinuationAndIndentation # ForIndentation
AccessModifierOffset: -4
ColumnLimit: 135
#AlignConsecutiveAssignments: true
AlignConsecutiveBitFields: true
AlignEscapedNewlines: DontAlign
AlignTrailingComments: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
#AllowShortLambdasOnASingleLine: Inline
AllowShortEnumsOnASingleLine: true
AllowAllArgumentsOnNextLine: true
AllowAllParametersOfDeclarationOnNextLine: true
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBraces: Custom
BraceWrapping:
AfterClass: true
AfterEnum: true
AfterStruct: true
AfterUnion: true
AfterFunction: true
SplitEmptyFunction: false
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeTernaryOperators: true
ConstructorInitializerAllOnOneLineOrOnePerLine: true
FixNamespaceComments: true
IncludeBlocks: Regroup
KeepEmptyLinesAtTheStartOfBlocks: false
PointerAlignment: Left
ReflowComments: true
SortIncludes: true

58
tools/chatgui/zxing-cpp/.clang-tidy

@ -0,0 +1,58 @@
---
Checks: >
-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
-bugprone-exception-escape,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-integer-division,
-bugprone-macro-parentheses,
-bugprone-narrowing-conversions,
-bugprone-signed-char-misuse,
-bugprone-use-after-move,
-bugprone-assignment-in-if-condition,
performance-*,
-performance-enum-size,
-performance-move-const-arg,
portability-*,
-portability-avoid-pragma-once,
readability-avoid-const-params-in-decls,
readability-const-return-type,
readability-delete-null-pointer,
readability-deleted-default,
readability-inconsistent-declaration-parameter-name,
readability-misleading-indentation,
readability-misplaced-array-index,
readability-non-const-parameter,
readability-redundant-*,
readability-simplify-*,
-readability-simplify-boolean-expr,
readability-static-*,
-readability-static-definition-in-anonymous-namespace,
readability-string-compare,
readability-suspicious-call-argument,
readability-uniqueptr-delete-release,
readability-use-anyofallof,
-readability-redundant-inline-specifier,
-readability-redundant-member-init,
modernize-deprecated-*,
modernize-loop-convert,
modernize-make-*,
modernize-pass-by-value,
modernize-raw-string-literal,
modernize-redundant-void-arg,
modernize-replace-*,
modernize-return-braced-init-list,
modernize-shrink-to-fit,
modernize-unary-static-assert,
modernize-use-*,
-modernize-use-constraints,
-modernize-use-designated-initializers,
-modernize-use-nodiscard,
-modernize-use-ranges,
-modernize-use-trailing-return-type,
-modernize-use-integer-sign-comparison,
WarningsAsErrors: ''
HeaderFilterRegex: '^(?!.*zint\.h).*$'
FormatStyle: file

19
tools/chatgui/zxing-cpp/.editorconfig

@ -0,0 +1,19 @@
root = true
[*]
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
[*.{cpp,c,h,html,py,kt,cs,qlm}]
indent_style = tab
indent_size = 4
max_line_length = 135
[CMakeLists.txt]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false

3
tools/chatgui/zxing-cpp/.gitattributes vendored

@ -0,0 +1,3 @@
test/samples export-ignore
zint export-ignore
.git* export-ignore

2
tools/chatgui/zxing-cpp/.github/FUNDING.yml

@ -0,0 +1,2 @@
github: axxel
custom: 'www.paypal.com/donate/?hosted_button_id=8LDF4NNQF8Z3L'

102
tools/chatgui/zxing-cpp/.github/create_release_archive.py

@ -0,0 +1,102 @@
#!/usr/bin/env python3
import subprocess
import tempfile
import shutil
import sys
import re
from pathlib import Path
def run_command(cmd, cwd=None):
try:
return subprocess.run(cmd, check=True, capture_output=True, text=True, cwd=cwd)
except subprocess.CalledProcessError as e:
print(f"✗ Error {cmd}: {e.stderr}")
sys.exit(1)
def main(repo_root=None, output_dir=None):
repo_root = Path.cwd() if repo_root is None else Path(repo_root).resolve()
output_dir = repo_root if output_dir is None else Path(output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Working in repository: {repo_root}")
# Extract version and optional suffix from core/CMakeLists.txt
cmake_txt = (repo_root / "core" / "CMakeLists.txt").read_text()
version = re.search(r'ZXing VERSION "?([0-9]+\.[0-9]+\.[0-9]+)"?', cmake_txt).group(1)
suffix = re.search(r'ZXING_VERSION_SUFFIX "?([^\)\s\"]*)"?', cmake_txt).group(1)
ver_tag = f"{version}{suffix}"
print(f"Project version from CMake: {ver_tag}")
# Create output filenames with git tag
output_stem = f"zxing-cpp-{ver_tag}"
output_tgz = output_dir / f"{output_stem}.tar.gz"
output_zip = output_dir / f"{output_stem}.zip"
# Step 1: Create initial tar ball via git archive
print("\n[1/4] Creating git archive...")
run_command(["git", "archive", f"--prefix={output_stem}/", f"--output={output_tgz}", "HEAD", repo_root])
print(f"✓ Created git archive: {output_tgz}")
# Step 2: Extract tar ball into temporary folder
print("\n[2/4] Extracting archive to temporary folder...")
with tempfile.TemporaryDirectory() as temp_dir:
print(f"✓ Created temporary directory: {temp_dir}")
run_command(["tar", "-xf", str(output_tgz), "-C", temp_dir])
print(f"✓ Extracted archive to temporary folder")
# Step 3: Replace files in core/src/libzint/ with files from zint/backend/
print("\n[3/4] Replacing files in core/src/libzint/ with zint/backend/ files...")
dst_dir = Path(temp_dir) / output_stem / "core" / "src" / "libzint"
src_dir = repo_root / "zint" / "backend" # Use actual repo directory
# Find all files/symlinks currently in core/src/libzint/ (including subdirectories)
libzint_files = list(dst_dir.rglob("*"))
# Include both regular files and symlinks
libzint_files = [f for f in libzint_files if f.is_file() or f.is_symlink()]
replaced_count = 0
not_found_count = 0
for libzint_file in libzint_files:
# Get relative path from core/src/libzint/
rel_path = libzint_file.relative_to(dst_dir)
# Source path in zint/backend/
source_file = src_dir / rel_path
# Only replace if the file exists in zint/backend/
if source_file.exists() and source_file.is_file():
# Remove symlink if it exists
if libzint_file.is_symlink():
libzint_file.unlink()
# Copy the actual file
shutil.copy2(source_file, libzint_file)
replaced_count += 1
else:
not_found_count += 1
print(f" - Skipped (not in zint/backend): {rel_path}")
print(f"✓ Replaced {replaced_count} files, skipped {not_found_count} files")
# Step 4: Create tar.gz and zip archives
print("\n[4/4] Creating archives (tar.gz and zip)...")
run_command(["tar", "-czf", str(output_tgz), "-C", temp_dir, output_stem])
if (output_zip.exists()):
output_zip.unlink()
run_command(["zip", "-q", "-r", str(output_zip), output_stem], cwd=temp_dir)
# Cleanup initial tar file
print(f"\n✅ Done! Source archives created:")
print(f" - {output_tgz}")
print(f" - {output_zip}")
if __name__ == "__main__":
repo_root = sys.argv[1] if len(sys.argv) > 1 else None
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
main(repo_root=repo_root, output_dir=output_dir)

435
tools/chatgui/zxing-cpp/.github/workflows/ci.yml

@ -0,0 +1,435 @@
name: CI
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
BUILD_TYPE: Release
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
# don't cancel all jobs just because one of them failed
fail-fast: false
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Setup Python 3
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Configure
run: >
cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON
-DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_DOTNET=ON -DZXING_TEST_GO=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=ON
-DZXING_PYTHON_MODULE=ON
- name: Build
run: cmake --build build --parallel --config ${{env.BUILD_TYPE}}
- name: Set PATH for Tests
shell: bash # to make the $GITHUB_PATH update work
if: runner.os == 'Windows'
run: |
echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH
echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH
- name: Test
run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}}
- name: Install
run: |
cmake -E make_directory install
cmake --install build --config ${{env.BUILD_TYPE}} --prefix ${{github.workspace}}/install
- uses: actions/upload-artifact@v6
with:
name: ${{matrix.os}}-artifacts
path: install
build-exp:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest, ubuntu-24.04-arm, macos-15-intel, windows-11-arm]
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Setup Python 3
id: setup-python
if: matrix.os == 'windows-11-arm' # the default python on the arm runner is an x64 build
uses: actions/setup-python@v6
with:
python-version: '3.13'
architecture: arm64
- name: Configure
run: >
cmake -S . -B build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_SHARED_LIBS=ON
-DZXING_READERS=ON -DZXING_WRITERS=BOTH -DZXING_EXPERIMENTAL_API=ON
-DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=OFF -DZXING_PYTHON_MODULE=ON
${{ steps.setup-python.outputs.python-path != '' && format('-DPython_EXECUTABLE={0}', steps.setup-python.outputs.python-path) || '' }}
- name: Build
run: cmake --build build --parallel --config ${{env.BUILD_TYPE}}
- name: Set PATH for Tests
shell: bash # to make the $GITHUB_PATH update work
if: runner.os == 'Windows'
run: |
echo "${GITHUB_WORKSPACE}/build/core/${BUILD_TYPE}" >> $GITHUB_PATH
echo "${GITHUB_WORKSPACE}/build/bin/${BUILD_TYPE}" >> $GITHUB_PATH
- name: Test
run: ctest --test-dir build -V -C ${{env.BUILD_TYPE}}
- name: Install
run: |
cmake -E make_directory install
cmake --install build --config ${{env.BUILD_TYPE}} --prefix ${{github.workspace}}/install
- uses: actions/upload-artifact@v6
with:
name: ${{matrix.os}}-exp-artifacts
path: install
build-ubuntu-gcc-12: # keep homebrew happy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Configure
run: >
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DZXING_WRITERS=BOTH
-DCMAKE_CXX_COMPILER=g++-12 -DCMAKE_C_COMPILER=gcc-12
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest -V --test-dir build
build-ubuntu-asan-ubsan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Configure
run: >
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DZXING_WRITERS=BOTH
-DZXING_BLACKBOX_TESTS=ON -DZXING_UNIT_TESTS=ON -DZXING_PYTHON_MODULE=OFF -DZXING_C_API=OFF
-DCMAKE_CXX_COMPILER=clang++
-DCMAKE_CXX_FLAGS="-march=native -fsanitize=address,undefined -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer"
-DCMAKE_C_COMPILER=clang
-DCMAKE_C_FLAGS="-march=native -fsanitize=address,undefined -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer"
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest -V --test-dir build
build-ubuntu-tsan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Configure
run: >
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DZXING_WRITERS=OFF -DZXING_EXAMPLES=OFF
-DZXING_BLACKBOX_TESTS=ON -DZXING_UNIT_TESTS=OFF -DZXING_PYTHON_MODULE=OFF -DZXING_C_API=OFF
-DCMAKE_CXX_COMPILER=clang++
-DCMAKE_CXX_FLAGS="-march=native -fsanitize=thread -fno-optimize-sibling-calls -fno-omit-frame-pointer -g"
-DCMAKE_C_COMPILER=clang
-DCMAKE_C_FLAGS="-march=native -fsanitize=thread -fno-optimize-sibling-calls -fno-omit-frame-pointer -g"
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest -V --test-dir build
build-old-writers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Configure
run: >
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DZXING_READERS=ON -DZXING_WRITERS=OLD -DZXING_PYTHON_MODULE=ON -DZXING_C_API=ON
-DZXING_BLACKBOX_TESTS=ON -DZXING_TEST_INSTALL=ON -DZXING_UNIT_TESTS=ON -DZXING_PYTHON_MODULE=ON
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest -V --test-dir build
build-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Build the swift package
run: swift build
- name: Build the demo app
working-directory: wrappers/ios/demo
run: xcodebuild build -scheme demo -sdk "iphonesimulator"
# - name: Validate the Pod
# run: pod lib lint --allow-warnings
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
- name: Build the lib/app
working-directory: wrappers/android
run: ./gradlew assembleRelease
- uses: actions/upload-artifact@v6
with:
name: android-artifacts
path: "wrappers/android/zxingcpp/build/outputs/aar/zxingcpp-release.aar"
- name: Publish Library Snapshot
if: github.repository == 'zxing-cpp/zxing-cpp' && github.event_name != 'pull_request'
working-directory: wrappers/android
env:
ORG_GRADLE_PROJECT_publishSnapshot: true
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }}
run: ./gradlew publishToMavenCentral --no-configuration-cache
build-kn:
runs-on: ubuntu-latest
defaults:
run:
working-directory: wrappers/kn
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: true
- name: Prepare Zig toolchain
uses: mlugg/setup-zig@v2
with:
version: 0.13.0
- name: Checkout toolchain initializer repository
uses: actions/checkout@v6
with:
repository: ISNing/kn-toolchain-initializer
path: wrappers/kn/.kn-toolchain-initializer
- name: Set up JDK
uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v5
- name: Initialize Kotlin/Native toolchain
working-directory: wrappers/kn/.kn-toolchain-initializer
run: ./gradlew build -DkotlinVersion=2.2.0
- name: Run test for linuxX64 target
run: |
echo -e "konan.dir=$HOME/.konan/kotlin-native-prebuilt-linux-x86_64-2.2.0" >> local.properties
./gradlew linuxX64Test
build-python:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ['3.12']
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install numpy pillow build
- name: Build module
working-directory: wrappers/python
run: python -m pip -v install -e .
- name: Test module
working-directory: wrappers/python
run: python -m unittest -v
- name: Build wheel
working-directory: wrappers/python
run: python -m build --wheel
- uses: actions/upload-artifact@v6
with:
name: ${{matrix.os}}-python-artifacts
path: wrappers/python/dist/*
build-rust:
runs-on: ubuntu-latest
defaults:
run:
working-directory: wrappers/rust
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Lint
run: |
cargo fmt --check
cargo clippy -- -Dwarnings
- name: Build
run: cargo build --release --verbose --all-features --examples
- name: Test
run: cargo test --release --all-features
- name: Package
# --allow-dirty is required on the windows build (but not the ubuntu build?!)
run: |
cargo package --all-features
cargo publish --dry-run
- uses: actions/upload-artifact@v6
with:
name: rust-artifacts
path: wrappers/rust/target/package/*.crate
build-wasm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- uses: emscripten-core/setup-emsdk@v15
- name: Configure
run: emcmake cmake -Swrappers/wasm -Bbuild
- name: Build
run: cmake --build build --parallel
# - name: Test
# run: node build/EmGlueTests.js
- uses: actions/upload-artifact@v6
with:
name: wasm-artifacts
path: |
build/zxing*
build/demo*
# disable WinRT build for now, as it does not compile with C++20 (see wrappers/winrt/CMakeLists.txt)
# build-winrt:
# runs-on: windows-latest
# steps:
# - uses: actions/checkout@v6
# with:
# submodules: true
# - name: Configure
# shell: cmd # powershell messes up the arguments containing a '.' ?!?
# run: >
# cmake -S wrappers/winrt -B build -A ARM64
# -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_BUILD_TYPE=Release
# -DBUILD_WINRT_LIB=ON -DZXING_EXAMPLES=OFF -DZXING_BLACKBOX_TESTS=OFF -DZXING_C_API=OFF
# -DEXTENSION_SDK_OUTPUT=dist/UAP/v0.8.0.0/ExtensionSDKs/ZXingWinRT/1.0.0.0
# - name: Build
# run: cmake --build build -j8 --config Release
# - uses: actions/upload-artifact@v6
# with:
# name: winrt-ARM64-artifacts
# path: build/dist
build-tarball:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Build
run: python .github/create_release_archive.py
- name: Find generated archives
id: find
run: |
tgz=$(ls zxing-cpp-*.tar.gz 2>/dev/null | head -n1 || true)
zip=$(ls zxing-cpp-*.zip 2>/dev/null | head -n1 || true)
echo "tgz=$tgz" >> $GITHUB_OUTPUT
echo "zip=$zip" >> $GITHUB_OUTPUT
- name: Upload source_artifacts
uses: actions/upload-artifact@v6
with:
name: source_artifacts
path: |
${{ steps.find.outputs.tgz }}
${{ steps.find.outputs.zip }}

59
tools/chatgui/zxing-cpp/.github/workflows/codeql-analysis.yml

@ -0,0 +1,59 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: CodeQL
on:
workflow_dispatch:
# push:
# branches: [ master ]
# pull_request:
# # The branches below must be a subset of the branches above
# branches: [ master ]
# schedule:
# - cron: '18 21 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
strategy:
fail-fast: false
matrix:
language: [ 'cpp' ] # , 'java-kotlin', 'swift' currently fail with autobuild, 'python' is not worth the extra build time
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: manual
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Build
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DZXING_EXPERIMENTAL_API=ON
cmake --build build -j
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4

71
tools/chatgui/zxing-cpp/.github/workflows/msvc-analysis.yml

@ -0,0 +1,71 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
#
# Find more information at:
# https://github.com/microsoft/msvc-code-analysis-action
name: Microsoft C++ Code Analysis
on: workflow_dispatch
# on:
# push:
# branches: [ "master" ]
# pull_request:
# branches: [ "master" ]
# schedule:
# - cron: '34 6 * * 1'
env:
# Path to the CMake build directory.
build: '${{ github.workspace }}/build'
permissions:
contents: read
jobs:
analyze:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
name: Analyze
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: true
- name: Configure CMake
run: cmake -B ${{ env.build }} -DZXING_EXPERIMENTAL_API=ON -DZXING_DISABLE_PCH=ON
# Build is not required unless generated source files are used
# - name: Build CMake
# run: cmake --build ${{ env.build }}
- name: Initialize MSVC Code Analysis
uses: microsoft/msvc-code-analysis-action@v0.1.1
# Provide a unique ID to access the sarif output path
id: run-analysis
with:
cmakeBuildDirectory: ${{ env.build }}
# Ruleset file that will determine what checks will be run
ruleset: NativeRecommendedRules.ruleset
# Upload SARIF file to GitHub Code Scanning Alerts
# this currently fails with the Error: The CodeQL Action does not support uploading multiple SARIF runs with the same category.
# - name: Upload SARIF to GitHub
# uses: github/codeql-action/upload-sarif@v4
# with:
# sarif_file: ${{ steps.run-analysis.outputs.sarif }}
# Upload SARIF file as an Artifact to download and view
- name: Upload SARIF as an Artifact
uses: actions/upload-artifact@v6
with:
name: sarif-file
path: ${{ steps.run-analysis.outputs.sarif }}

41
tools/chatgui/zxing-cpp/.github/workflows/publish-android.yml

@ -0,0 +1,41 @@
name: publish-android
on:
# release:
# types: [published]
workflow_dispatch:
inputs:
publish:
description: 'Publish package (y/n)'
default: 'n'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
# working-directory: wrappers/android
# run: ./gradlew versionDisplay
- name: Build Library
working-directory: wrappers/android
run: ./gradlew assembleRelease
- name: Publish Library
if: ${{ github.event.inputs.publish == 'y' }}
working-directory: wrappers/android
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }}
run: ./gradlew publishToMavenCentral --no-configuration-cache
# To actually publish the the blob, login to https://central.sonatype.com/publishing/deployments.

130
tools/chatgui/zxing-cpp/.github/workflows/publish-dotnet.yml

@ -0,0 +1,130 @@
name: publish-dotnet
on:
# release:
# types: [published]
workflow_dispatch:
inputs:
publish:
description: 'Publish package (y/n)'
default: 'n'
jobs:
build-native:
name: build-native ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
lib_name: ZXing.dll
rid: win-x64
lib_path: build/core/Release/ZXing.dll
- os: windows-11-arm
lib_name: ZXing.dll
rid: win-arm64
lib_path: build/core/Release/ZXing.dll
- os: ubuntu-latest
lib_name: libZXing.so
rid: linux-x64
lib_path: build/core/libZXing.so
- os: ubuntu-24.04-arm
lib_name: libZXing.so
rid: linux-arm64
lib_path: build/core/libZXing.so
- os: macos-15-intel
lib_name: libZXing.dylib
rid: osx-x64
lib_path: build/core/libZXing.dylib
- os: macos-latest
lib_name: libZXing.dylib
rid: osx-arm64
lib_path: build/core/libZXing.dylib
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Configure CMake
run: >
cmake -S . -B build
-DCMAKE_BUILD_TYPE=Release
-DBUILD_SHARED_LIBS=ON
-DZXING_EXAMPLES=OFF
-DZXING_TEST_DOTNET=ON
- name: Build
run: cmake --build build --config Release --parallel
- name: Test
run: ctest --test-dir build -V -C Release -R DotNetTest
- name: Stage native library
run: |
cmake -E make_directory artifact/runtimes/${{ matrix.rid }}/native
cmake -E copy_if_different ${{ matrix.lib_path }} artifact/runtimes/${{ matrix.rid }}/native/${{ matrix.lib_name }}
- name: Upload native library
uses: actions/upload-artifact@v6
with:
name: native-${{ matrix.rid }}
path: artifact
publish-nuget:
needs: build-native
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Download native libraries
uses: actions/download-artifact@v8
with:
pattern: native-*
path: wrappers/dotnet
merge-multiple: true
- name: Validate runtime files
shell: bash
run: |
test -f wrappers/dotnet/runtimes/win-x64/native/ZXing.dll
test -f wrappers/dotnet/runtimes/win-arm64/native/ZXing.dll
test -f wrappers/dotnet/runtimes/linux-x64/native/libZXing.so
test -f wrappers/dotnet/runtimes/linux-arm64/native/libZXing.so
test -f wrappers/dotnet/runtimes/osx-x64/native/libZXing.dylib
test -f wrappers/dotnet/runtimes/osx-arm64/native/libZXing.dylib
- name: Pack NuGet package
run: dotnet pack wrappers/dotnet/ZXingCpp/ZXingCpp.csproj --configuration Release --output wrappers/dotnet/artifacts
- name: Smoke test local NuGet package
shell: bash
run: |
set -euo pipefail
pkg_file="$(ls wrappers/dotnet/artifacts/ZXingCpp.*.nupkg | head -n 1)"
pkg_version="$(basename "$pkg_file" | sed -E 's/^ZXingCpp\.//; s/\.nupkg$//')"
tmp_dir="$(mktemp -d)"
dotnet new console -n ZxingPkgTest -o "$tmp_dir/ZxingPkgTest"
echo "Console.WriteLine(global::ZXingCpp.BarcodeFormat.QRCode.ToString());" > "$tmp_dir/ZxingPkgTest/Program.cs"
dotnet add "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" package ZXingCpp --version "$pkg_version" --no-restore
dotnet restore "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" \
--source "${{ github.workspace }}/wrappers/dotnet/artifacts" \
--source https://api.nuget.org/v3/index.json
dotnet run --project "$tmp_dir/ZxingPkgTest/ZxingPkgTest.csproj" --no-restore
- name: Upload NuGet package artifact
uses: actions/upload-artifact@v6
with:
name: dotnet-nuget-package
path: wrappers/dotnet/artifacts/*.nupkg
- name: Push NuGet package
if: ${{ github.event_name == 'release' || github.event.inputs.publish == 'y' }}
run: dotnet nuget push wrappers/dotnet/artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }} --skip-duplicate

131
tools/chatgui/zxing-cpp/.github/workflows/publish-gh-pages.yml

@ -0,0 +1,131 @@
name: publish-gh-pages
on:
# Runs on pushes targeting the default branch
# push:
# branches: ["master"]
# Runs on new releases
release:
types: [published]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build-wasm:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: true
- name: Setup emsdk
uses: emscripten-core/setup-emsdk@v15
# with:
# # Make sure to set a version number!
# version: 1.38.40
# # This is the name of the cache folder.
# # The cache folder will be placed in the build directory,
# # so make sure it doesn't conflict with anything!
# actions-cache-folder: 'emsdk-cache'
- name: Configure
run: emcmake cmake -Swrappers/wasm -Bbuild
- name: Build
run: cmake --build build --parallel
- name: Prepare Archive
shell: sh
run: |
mkdir -p public/demo
mv build/zxing_* build/*.html public/demo/
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
path: 'public'
name: 'artifact-wasm'
build-docs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Install dependencies
run: brew install doxygen
- name: Configure
run: cmake -S . -B build
- name: Build docs
run: cmake --build build --target docs
- name: Prepare deploy folder
run: |
mkdir -p public/docs
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
mkdir -p public/docs/$VERSION
cp -r build/docs/html/* public/docs/$VERSION/
else
mkdir -p public/docs/latest
cp -r build/docs/html/* public/docs/latest/
fi
- uses: actions/upload-artifact@v7
with:
path: public
name: 'artifact-docs'
deploy:
needs: [build-wasm, build-docs]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Download WASM artifact
uses: actions/download-artifact@v8
with:
name: artifact-wasm
path: wasm
- name: Download Docs artifact
uses: actions/download-artifact@v8
with:
name: artifact-docs
path: docs
- name: Merge artifacts
run: |
mkdir public
cp -r wasm/* public/
cp -r docs/* public/
- name: Upload merged artifact
uses: actions/upload-pages-artifact@v4
with:
path: public
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5

97
tools/chatgui/zxing-cpp/.github/workflows/publish-kn.yml

@ -0,0 +1,97 @@
name: publish-kn
on:
workflow_dispatch:
inputs:
publish:
description: 'Publish package (y/n)'
default: 'n'
jobs:
publish:
name: Library Publish
runs-on: macos-latest # at least macos-13 is required to enable c++20 support
defaults:
run:
working-directory: ./wrappers/kn
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
submodules: true
- name: Prepare Zig toolchain
uses: mlugg/setup-zig@v2
with:
version: 0.13.0
- name: Checkout toolchain initializer repository
uses: actions/checkout@v6
with:
repository: ISNing/kn-toolchain-initializer
path: wrappers/kn/.kn-toolchain-initializer
- name: Set up JDK
uses: actions/setup-java@v5
with:
java-version: 17
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@v5
- name: Initialize Kotlin/Native toolchain
working-directory: wrappers/kn/.kn-toolchain-initializer
run: ./gradlew build -DkotlinVersion=2.2.0
- name: Export Toolchain properties
run: |
echo -e "konan.dir=$HOME/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.0" >> local.properties
- name: Build Library
run: |
export PATH="$PATH:$(brew --prefix llvm@15)/bin"
./gradlew assemble
- name: Run All Library Tests
run: |
export PATH="$PATH:$(brew --prefix llvm@15)/bin"
./gradlew allTests
- name: Upload Library Test Reports
uses: actions/upload-artifact@v6
with:
name: allTests-reports
path: wrappers/kn/build/reports/tests/allTests
- name: Upload Built LinuxArm64 shared library
uses: actions/upload-artifact@v6
with:
name: linuxArm64-shared
path: wrappers/kn/build/cmake/libZXing/linuxArm64
- name: Upload Built LinuxX64 shared library
uses: actions/upload-artifact@v6
with:
name: linuxX64-shared
path: wrappers/kn/build/cmake/libZXing/linuxX64
- name: Publish Library
if: ${{ github.event.inputs.publish == 'y' }}
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }}
run: |
export PATH="$PATH:$(brew --prefix llvm@15)/bin"
./gradlew publishToMavenCentral
# To actually publish the blob, login to https://central.sonatype.com/publishing/deployments.

100
tools/chatgui/zxing-cpp/.github/workflows/publish-python.yml

@ -0,0 +1,100 @@
name: publish-python
on:
# release:
# types: [published]
workflow_dispatch:
inputs:
publish:
description: 'Publish package (y/n)'
default: 'n'
# push:
# branches: [master]
# tags: ["v*.*.*"]
# pull_request:
# branches: [master]
jobs:
build-wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest, macos-15-intel, ubuntu-24.04-arm, windows-11-arm]
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install cibuildwheel
run: python3 -m pip install cibuildwheel==3.3.0
- name: Build wheels
run: python3 -m cibuildwheel --output-dir wheelhouse wrappers/python
- name: Upload wheels
uses: actions/upload-artifact@v6
with:
name: cibw-wheels-${{ matrix.os }}
path: ./wheelhouse/*.whl
build-sdist:
name: Build source distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Install dependencies
working-directory: wrappers/python
run: python -m pip install --upgrade build
- name: Build sdist
working-directory: wrappers/python
run: python3 -m build --sdist
- name: Upload sdist
uses: actions/upload-artifact@v6
with:
name: cibw-sdist
path: wrappers/python/dist/*.tar.gz
upload-pypi:
name: Upload to PyPI
needs: [build-wheels, build-sdist]
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/zxing-cpp
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
# only run if the commit is tagged...
# if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
if: github.event_name == 'release' || github.event.inputs.publish == 'y'
steps:
- uses: actions/download-artifact@v8
with:
pattern: cibw-*
path: dist
merge-multiple: true
- name: List wheels
run: ls dist
- uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository-url: https://test.pypi.org/legacy/

39
tools/chatgui/zxing-cpp/.github/workflows/publish-source.yml

@ -0,0 +1,39 @@
name: publish-source
on:
release:
types: [published]
workflow_dispatch:
jobs:
publish-source:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: true
- name: Create source archives
run: python .github/create_release_archive.py
- name: Create test samples tarball
run: tar -czf test_samples.tar.gz test/samples
- name: Upload workflow artifact
if: github.event_name != 'release'
uses: actions/upload-artifact@v6
with:
name: source-archives
path: |
*.zip
*.gz
if-no-files-found: error
- name: Upload to release
if: github.event_name == 'release'
run: |
tag='${{ github.event.release.tag_name }}'
version="${tag#v}"
gh release upload --clobber "$tag" "$GITHUB_WORKSPACE/zxing-cpp-$version.tar.gz" "$GITHUB_WORKSPACE/zxing-cpp-$version.zip" "$GITHUB_WORKSPACE/test_samples.tar.gz"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

82
tools/chatgui/zxing-cpp/.github/workflows/publish-winrt.yml

@ -0,0 +1,82 @@
name: publish-winrt
on:
# release:
# types: [published]
workflow_dispatch:
inputs:
publish:
description: 'Publish package (y/n)'
default: 'n'
package_version:
description: 'Package version (major.minor.patch)'
default: '1.0.0'
jobs:
build:
runs-on: windows-latest
strategy:
matrix:
target: [Win32, x64, ARM64]
steps:
- uses: actions/checkout@v6
- name: Create build environment
shell: cmd
run: |
cmake -E make_directory ${{runner.workspace}}/build
cmake -E copy_directory ${{github.workspace}}/wrappers/winrt/UAP ${{runner.workspace}}/build/dist/UAP
cmake -E copy_directory ${{github.workspace}}/wrappers/winrt/nuget ${{runner.workspace}}/build/dist/nuget
- name: Configure CMake
shell: cmd
working-directory: ${{runner.workspace}}/build
run: cmake ${{github.workspace}}/wrappers/winrt -A ${{matrix.target}} -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_BUILD_TYPE=Release -DBUILD_WINRT_LIB=ON -DZXING_EXAMPLES=OFF -DZXING_BLACKBOX_TESTS=OFF -DEXTENSION_SDK_OUTPUT=dist/UAP/v0.8.0.0/ExtensionSDKs/ZXingWinRT/1.0.0.0
- name: Build
shell: cmd
working-directory: ${{runner.workspace}}/build
run: cmake --build . -j8 --config Release
- uses: actions/upload-artifact@v6
with:
name: winrt-${{matrix.target}}-artifacts
path: ${{runner.workspace}}/build/dist
publish-package:
needs: build
runs-on: windows-latest
if: ${{ github.event_name == 'release' || github.event.inputs.publish == 'y' }}
steps:
- uses: actions/download-artifact@v8
with:
name: winrt-Win32-artifacts
- uses: actions/download-artifact@v8
with:
name: winrt-x64-artifacts
- uses: actions/download-artifact@v8
with:
name: winrt-ARM64-artifacts
- name: Create NuGet package
if: ${{ github.event_name != 'release' }}
shell: cmd
run: nuget pack -Version ${{ github.event.inputs.package_version }} -OutputFileNamesWithoutVersion nuget/ZXingWinRT.nuspec
- name: Create NuGet package
if: ${{ github.event_name == 'release' }}
shell: bash
env:
RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
run: nuget pack -Version ${RELEASE_TAG_NAME:1} -OutputFileNamesWithoutVersion nuget/ZXingWinRT.nuspec
- name: Publish NuGet package
shell: cmd
run: nuget push huycn.zxingcpp.winrt.nupkg -ApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json
- uses: actions/upload-artifact@v6
with:
name: nuget-package
path: huycn.zxingcpp.winrt.nupkg

27
tools/chatgui/zxing-cpp/.gitignore vendored

@ -0,0 +1,27 @@
*.o
*.so
*.lib
*.d
*.a
compile_commands.json
/build
# QtCreator
CMakeLists.txt.user
# Vim
*.swp
# XCode/Swift
.swiftpm
.build
# JetBrain (AndroidStudio, clion)
.idea
# Visual Studio
/.vs
/CMakeUserPresets.json
# Visual Studio Code
/.vscode

4
tools/chatgui/zxing-cpp/.gitmodules vendored

@ -0,0 +1,4 @@
[submodule "zint"]
path = zint
url = https://github.com/zint/zint.git
shallow = true

121
tools/chatgui/zxing-cpp/CMakeLists.txt

@ -0,0 +1,121 @@
cmake_minimum_required(VERSION 3.16)
project(ZXing)
option (ZXING_READERS "Build with reader support (decoders)" ON)
set (ZXING_WRITERS "ON" CACHE STRING "Build with old and/or new writer (encoder) backend (OFF/ON/OLD/NEW/BOTH)")
option (ZXING_C_API "Build the C-API" ON)
option (ZXING_EXPERIMENTAL_API "Build with experimental API" OFF)
option (ZXING_EXAMPLES "Build the example barcode reader/writer applications" ON)
option (ZXING_EXAMPLES_QT "Build the Qt based example barcode reader/writer applications" OFF)
option (ZXING_BLACKBOX_TESTS "Build the black box reader/writer tests" OFF)
option (ZXING_UNIT_TESTS "Build the unit tests (don't enable for production builds)" OFF)
option (ZXING_TEST_DOTNET "Test if the .NET wrapper works" OFF)
option (ZXING_TEST_GO "Test if the Go wrapper works" OFF)
option (ZXING_TEST_INSTALL "Test if the installed/public API works" OFF)
option (ZXING_PYTHON_MODULE "Build the python module" OFF)
set (ZXING_DEPENDENCIES "AUTO" CACHE STRING "Fetch from github or use locally installed (AUTO/GITHUB/LOCAL)")
option (ZXING_CLANG_TIDY "Enable clang-tidy analysis" OFF)
if (WIN32)
option (BUILD_SHARED_LIBS "Build and link as shared library" OFF)
else()
option (BUILD_SHARED_LIBS "Build and link as shared library" ON)
endif()
if (MSVC)
add_definitions (-DUNICODE -D_UNICODE -D_CRT_SECURE_NO_WARNINGS)
option (ZXING_LINK_CPP_STATICALLY "MSVC only, link standard library statically (/MT and /MTd)" OFF)
if (LINK_CPP_STATICALLY OR ZXING_LINK_CPP_STATICALLY)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
endif()
if (NOT CMAKE_BUILD_TYPE)
set (DEFAULT_BUILD_TYPE "Release")
message (STATUS "Setting build type to '${DEFAULT_BUILD_TYPE}' as none was specified.")
set (CMAKE_BUILD_TYPE "${DEFAULT_BUILD_TYPE}" CACHE STRING "Choose the type of build." FORCE)
set_property (CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()
if (NOT (ZXING_READERS OR ZXING_WRITERS))
message(FATAL_ERROR "At least one of ZXING_READERS/ZXING_WRITERS must be enabled.")
endif()
set(ZXING_WRITERS_LIST OFF ON OLD NEW BOTH)
set_property(CACHE ZXING_WRITERS PROPERTY STRINGS ${ZXING_WRITERS_LIST})
if(NOT ZXING_WRITERS IN_LIST ZXING_WRITERS_LIST)
message(FATAL_ERROR "ZXING_WRITERS must be one of ${ZXING_WRITERS_LIST}")
endif()
set(ZXING_DEPENDENCIES_LIST AUTO GITHUB LOCAL)
set_property(CACHE ZXING_DEPENDENCIES PROPERTY STRINGS ${ZXING_DEPENDENCIES_LIST})
if(NOT ZXING_DEPENDENCIES IN_LIST ZXING_DEPENDENCIES_LIST)
message(FATAL_ERROR "ZXING_DEPENDENCIES must be one of ${ZXING_DEPENDENCIES_LIST}")
endif()
if (NOT DEFINED CMAKE_CXX_STANDARD)
set (CMAKE_CXX_STANDARD 20)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_CXX_EXTENSIONS OFF)
endif()
set(CMAKE_COLOR_DIAGNOSTICS ON)
if (ZXING_CLANG_TIDY)
find_program(CMAKE_CXX_CLANG_TIDY NAMES "clang-tidy" DOC "Path to clang-tidy executable")
if (NOT CMAKE_CXX_CLANG_TIDY)
message(WARNING "clang-tidy not found.")
else()
message(STATUS "clang-tidy found: ${CMAKE_CXX_CLANG_TIDY}")
set(ZXING_DISABLE_PCH ON)
endif()
endif()
# make installed binaries relocatable on macOS and Linux
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) # improve build reproducibility
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE)
set(CMAKE_INSTALL_RPATH "$<$<PLATFORM_ID:Darwin>:@loader_path/../lib>$<$<PLATFORM_ID:Linux>:$ORIGIN/../lib>")
add_subdirectory (core)
add_subdirectory (docs)
enable_testing()
include(zxing.cmake)
if (ZXING_EXAMPLES)
add_subdirectory (example)
endif()
if (ZXING_EXAMPLES_QT)
add_subdirectory (wrappers/qt)
endif()
if (ZXING_BLACKBOX_TESTS)
add_subdirectory (test/blackbox)
endif()
if (ZXING_UNIT_TESTS)
add_subdirectory (test/unit)
endif()
if (ZXING_TEST_DOTNET)
add_subdirectory (wrappers/dotnet)
endif()
if (ZXING_TEST_GO)
add_subdirectory (wrappers/go)
endif()
if (ZXING_TEST_INSTALL)
add_test(
NAME InstallTest
COMMAND
${CMAKE_COMMAND}
-DPROJECT_BINARY_DIR=${CMAKE_BINARY_DIR}
-DSOURCE_DIR=${CMAKE_SOURCE_DIR}/test/install
-DZXING_WRITERS=${ZXING_WRITERS}
-P ${CMAKE_SOURCE_DIR}/test/install/run.cmake
)
endif()
if (ZXING_PYTHON_MODULE)
add_subdirectory (wrappers/python)
endif()
if (ZXING_C_API)
add_subdirectory (wrappers/c)
endif()

202
tools/chatgui/zxing-cpp/LICENSE

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

38
tools/chatgui/zxing-cpp/Package.swift

@ -0,0 +1,38 @@
// swift-tools-version:5.7.1
import PackageDescription
let package = Package(
name: "ZXingCpp",
platforms: [
.macOS(.v13), .iOS(.v12)
],
products: [
.library(
name: "ZXingCpp",
targets: ["ZXingCpp"])
],
targets: [
.target(
name: "ZXingCppCore",
path: "core/src",
exclude: ["libzint", "ZXingC.cpp", "ZXingCpp.cpp"],
publicHeadersPath: ".",
cxxSettings: [
.headerSearchPath("../../wrappers/ios/Sources/Wrapper"),
.define("ZXING_INTERNAL")
]
),
.target(
name: "ZXingCpp",
dependencies: ["ZXingCppCore"],
path: "wrappers/ios/Sources/Wrapper",
publicHeadersPath: ".",
linkerSettings: [
.linkedFramework("CoreGraphics"),
.linkedFramework("CoreImage"),
.linkedFramework("CoreVideo")
]
)
],
cxxLanguageStandard: CXXLanguageStandard.cxx20
)

154
tools/chatgui/zxing-cpp/README.md

@ -0,0 +1,154 @@
[![Build Status](https://github.com/zxing-cpp/zxing-cpp/actions/workflows/ci.yml/badge.svg)](https://github.com/zxing-cpp/zxing-cpp/actions?query=workflow%3ACI)
# ZXing-C++
ZXing-C++ ("zebra crossing") is an open-source, multi-format linear/matrix barcode image processing library implemented in C++.
It was originally ported from the Java [ZXing library](https://github.com/zxing/zxing) but has been developed further and now includes many improvements in terms of runtime and detection performance. It can both read and write barcodes in a number of formats. Since version 3.0 the default writing backend is provided by the [zint library](https://sourceforge.net/projects/zint/).
## Features
* Written in pure C++20 (public API is C++17 compatible), no third-party dependencies (for the library itself)
* Thread safe
* Wrappers/Bindings for:
| | | |
|:-|:-|:-|
| [Android](wrappers/android/README.md) | [C](wrappers/c/README.md) | [Go](wrappers/go/README.md) |
| [iOS](wrappers/ios/README.md) | [Kotlin/Native](wrappers/kn/README.md) | [.NET](wrappers/dotnet/README.md) |
| [Python](wrappers/python/README.md) | [Qt](wrappers/qt/README.md) | [Rust](wrappers/rust/README.md) |
| [WebAssembly](wrappers/wasm/README.md) | [WinRT](wrappers/winrt/README.md) | |
| External projects: |
| [Flutter](https://pub.dev/packages/flutter_zxing) | [Julia](https://github.com/rakeshksr/ZXingCPP.jl) | |
## Supported Formats
| Symbology | Variants |
|:----------|:---------|
| ***Retail:*** | *(Point-of-Sale, Coupons)*
| EAN/UPC | EAN-13, EAN-8, EAN-5ᵂ, EAN-2ᵂ, UPC-A, UPC-E, ISBN
| DataBar | Omnidirectional, Stacked, Limited, Expanded, Expanded Stacked
| ***Industrial:*** | *(Logistics, Tracking, Pharma)*
| Code39 | Standard, Extended, PZN, Code32, (VIN, LOGMARS)
| Code93 |
| Code128 |
| ITF | ITF-14, (DHL Leitcode, DHL Identcode)
| Telepen | Telepen Alpha, Telepen Numeric
| ***Matrix:*** | *(Documents, Tickets, Logistics, IDs)*
| Aztec Code | Aztec Code, Aztec Rune
| Data Matrix | ECC200
| MaxiCode | (partial read support)
| PDF417 | PDF417, Compact PDF417, MicroPDF417
| QR Code | Model 1ᴿ, Model 2, Micro QR Code, rMQR
| ***Other:*** | *(Legacy, Niche)*
| Codabar |
| DXFilmEdge |
[Note:]
* ᵂ : write support only
* ᴿ : read support only
* DataBar used to be called RSS.
* DataBar, DX Film Edge, MaxiCode, Micro QR Code and rMQR Code are not supported for writing if the library is configured with `ZXING_WRITERS=OLD`.
## Sponsors
You can sponsor this library at [GitHub Sponsors](https://github.com/sponsors/axxel).
| | Named Sponsors: |
|:-:|:-|
| [![ArAllAll](https://www.alliedvision.com/favicon.ico)](https://www.alliedvision.com/) | [Allied Vision Konstanz GmbH](https://www.alliedvision.com/) |
| [![jknlsn](https://avatars.githubusercontent.com/u/9172090?s=32)](https://github.com/jknlsn) | [Jake Nelson](https://github.com/jknlsn) |
| [![KURZ](https://avatars.githubusercontent.com/u/25196688?s=32)](https://github.com/kurzdigital) | [KURZ Digital Solutions GmbH & Co. KG](https://github.com/kurzdigital) |
| [![Moonshine AI](https://avatars.githubusercontent.com/u/98664891?s=32)](https://github.com/moonshine-ai) | [Moonshine AI](https://github.com/moonshine-ai) |
| [![SAP](https://avatars.githubusercontent.com/u/2531208?s=32)](https://www.sap.com/germany/about/company/innovation/open-source.html) | [SAP (Open Source Program Office)](https://www.sap.com/germany/about/company/innovation/open-source.html) |
| [![Somco Software](https://avatars.githubusercontent.com/u/41477867?s=32)](https://github.com/somcosoftware) | [Somco Software](https://somcosoftware.com/en) |
| [![synedra](https://www.synedra.com/favicon.ico)](https://synedra.com/) | [synedra information technologies GmbH](https://synedra.com/) |
Thanks a lot for your contribution!
## Getting Started
### To read barcodes:
1. Load your image into memory (3rd-party library required).
2. Call `ReadBarcodes()` from [`ReadBarcode.h`](core/src/ReadBarcode.h), the simplest API to get a list of `Barcode` objects.
A very simple example looks like this:
```c++
#include "ZXing/ZXingCpp.h"
#include <iostream>
int main(int argc, char** argv)
{
int width, height;
unsigned char* data;
// load your image data from somewhere. ImageFormat::Lum assumes grey scale image data.
auto image = ZXing::ImageView(data, width, height, ZXing::ImageFormat::Lum);
auto options = ZXing::ReaderOptions().formats(ZXing::BarcodeFormat::QRCode);
auto barcodes = ZXing::ReadBarcodes(image, options);
for (const auto& b : barcodes)
std::cout << ZXing::ToString(b.format()) << ": " << b.text() << "\n";
return 0;
}
```
To see the full capability of the API, have a look at [`ZXingReader.cpp`](example/ZXingReader.cpp).
### To write barcodes:
1. Create a `Barcode` object with `CreateBarcodeFrom...()` from [`CreateBarcode.h`](core/src/CreateBarcode.h).
2. The `Barcode::symbol()` can be used to get access to the bit matrix (1 module == 1 pixel, no quiet zone)
3. Alternatively the 3 `WriteBarcodeTo...()` functions from [`WriteBarcode.h`](core/src/WriteBarcode.h) can be used to create an `Image`, a SVG string or a UTF-8 string representation.
A very simple example looks like this:
```c++
#include "ZXing/ZXingCpp.h"
#include <iostream>
int main(int argc, char** argv)
{
auto barcode = ZXing::CreateBarcodeFromText("some text", ZXing::BarcodeFormat::QRCode);
auto svg = ZXing::WriteBarcodeToSVG(barcode);
// see also ZXing::WriteBarcodeToImage()
std::cout << svg << "\n";
return 0;
}
```
As an example for how to parameterize the process with `CreatorOptions` and `WriterOptions`, have a look at [`ZXingWriter.cpp`](example/ZXingWriter.cpp).
### Online API documentation
The latest API documentation can be found here: https://zxing-cpp.github.io/zxing-cpp/docs/latest
## Web Demos
- [Read barcodes](https://zxing-cpp.github.io/zxing-cpp/demo/demo_reader.html)
- [Write barcodes](https://zxing-cpp.github.io/zxing-cpp/demo/demo_writer.html)
- [Read barcodes from camera](https://zxing-cpp.github.io/zxing-cpp/demo/demo_cam_reader.html)
[Note: those live demos are not necessarily fully up-to-date at all times.]
## Build Instructions
These are the generic instructions to build the library on Windows/macOS/Linux. For details on how to build the individual wrappers, follow the links above.
1. Make sure [CMake](https://cmake.org) version 3.16 or newer is installed. The python module requires 3.18 or higher.
2. Make sure a sufficiently C++20 compliant compiler is installed (minimum VS 2019 16.10? / gcc 11 / clang 12?).
3. See the cmake `ZXING_...` options to enable the testing code, python wrapper, etc.
```
git clone https://github.com/zxing-cpp/zxing-cpp.git --recursive --depth 1
cmake -S zxing-cpp -B zxing-cpp/build -DCMAKE_BUILD_TYPE=Release
cmake --build zxing-cpp/build --parallel --config Release
```
[Note: ready to use packages are available for/as
[vcpkg](https://github.com/Microsoft/vcpkg/tree/master/ports/nu-book-zxing-cpp),
[conan](https://github.com/conan-io/conan-center-index/tree/master/recipes/zxing-cpp),
[mingw](https://github.com/msys2/MINGW-packages/tree/master/mingw-w64-zxing-cpp),
[homebrew](https://formulae.brew.sh/formula/zxing-cpp) and a bunch of
[linux distributions](https://repology.org/project/zxing-cpp/versions).]

29
tools/chatgui/zxing-cpp/docs/CMakeLists.txt

@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.16)
project(ZXingDocs VERSION 3.1.0)
find_package(Doxygen)
if(DOXYGEN_FOUND)
include(FetchContent)
FetchContent_Declare(
doxygen-awesome-css
URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/heads/main.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(doxygen-awesome-css)
# Save the location the files were cloned into
# This allows us to get the path to doxygen-awesome.css
FetchContent_GetProperties(doxygen-awesome-css SOURCE_DIR AWESOME_CSS_DIR)
# Generate the Doxyfile
set(DOXYFILE_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in)
set(DOXYFILE_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
configure_file(${DOXYFILE_IN} ${DOXYFILE_OUT} @ONLY)
add_custom_target(docs
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYFILE_OUT}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
VERBATIM)
endif()

1373
tools/chatgui/zxing-cpp/docs/Doxyfile.in

File diff suppressed because it is too large Load Diff

58
tools/chatgui/zxing-cpp/example/CMakeLists.txt

@ -0,0 +1,58 @@
zxing_add_package_stb()
option (ZXING_EXAMPLES_USE_WEBP "Add support for WebP image decoding" OFF)
if (ZXING_EXAMPLES_USE_WEBP)
zxing_add_package(WebP WebP https://github.com/webmproject/libwebp.git v1.6.0)
endif()
include (GNUInstallDirs)
if (ZXING_WRITERS AND ZXING_ENABLE_QRCODE)
add_executable (ZXingWriter ZXingWriter.cpp)
target_link_libraries (ZXingWriter ZXing::ZXing stb::stb)
add_test(NAME ZXingWriterTest COMMAND ZXingWriter qrcode "I have the best words." test.png)
install(TARGETS ZXingWriter DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
if (ZXING_READERS)
add_executable (ZXingReader ZXingReader.cpp)
target_link_libraries (ZXingReader ZXing::ZXing stb::stb)
if (ZXING_EXAMPLES_USE_WEBP)
target_link_libraries (ZXingReader ZXing::ZXing stb::stb WebP::webp)
target_compile_definitions(ZXingReader PRIVATE ZXING_USE_WEBP)
endif()
add_test(NAME ZXingReaderTest COMMAND ZXingReader -fast -format qrcode test.png) # see above
set_tests_properties(ZXingReaderTest PROPERTIES DEPENDS ZXingWriterTest)
if (ZXING_EXAMPLES_USE_WEBP AND WIN32)
add_custom_command(TARGET ZXingReader POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:ZXingReader>
$<TARGET_FILE_DIR:ZXingReader>
COMMAND_EXPAND_LISTS
VERBATIM
)
endif()
install(TARGETS ZXingReader DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
if (ZXING_READERS)
find_package(OpenCV QUIET)
if (OpenCV_FOUND)
add_executable (ZXingOpenCV ZXingOpenCV.cpp)
target_include_directories (ZXingOpenCV PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries (ZXingOpenCV ZXing::ZXing ${OpenCV_LIBS})
else ()
message("INFO: OpenCV not found, skipping ZXingOpenCV example")
endif()
endif()
add_executable (ZXingBarcodeFormat ZXingBarcodeFormat.cpp)
target_link_libraries (ZXingBarcodeFormat ZXing::ZXing)

106
tools/chatgui/zxing-cpp/example/ZXingBarcodeFormat.cpp

@ -0,0 +1,106 @@
/*
* Copyright 2026 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "BarcodeFormat.h"
#include <version>
#ifdef __cpp_lib_format
#include "StdPrint.h"
#include <cctype>
#include <cstring>
#include <format>
#include <functional>
#include <string>
static void PrintUsage(const char* exePath)
{
std::print("ZXingBarcodeFormat - A command line tool to generate wrapper source code for the BarcodeFormat enum\n\n");
std::println("Usage: {} <Android|C#|Go|K/N|Rust|Swift>", exePath);
}
static void PrintBFs(
std::format_string<std::string, int> fmt,
std::function<std::string(const char*)> name = [](const char* name) { return std::string(name); })
{
std::println(fmt, name("Invalid"), 0xffff);
#define X(NAME, SYM, VAR, FLAGS, ZINT, ENABLED, HRI) std::println(fmt, name(#NAME), ZX_BCF_ID(SYM, VAR));
ZX_BCF_LIST(X)
#undef X
}
int main(int argc, char* argv[])
{
auto is = [&, i=1](const char* str) { return strncmp(argv[i], str, strlen(argv[i])) == 0; };
if (argc != 2) {
PrintUsage(argv[0]);
return -1;
} else if (is("Go")) {
std::println("package zxingcpp\n");
std::println("const (");
PrintBFs(" BarcodeFormat{:14} BarcodeFormat = 0x{:04X}");
std::println(")");
} else if (is("C#")) {
PrintBFs(" public static readonly BarcodeFormat {:15} = new BarcodeFormat(0x{:04X});");
} else if (is("K/N")) {
PrintBFs(" {0:15}(ZXing_BarcodeFormat.ZXing_BarcodeFormat_{0}),");
} else if (is("Rust")) {
PrintBFs("pub const ZXing_BarcodeFormat_{}: ZXing_BarcodeFormat = 0x{:04X};");
} else if (is("Swift")) {
auto swiftName = [](const char* name) {
std::string sv(name);
std::string ret = (char)std::tolower(sv[0]) + sv.substr(1);
// Convert to camelCase, but keep uppercase letters if they are followed by a lowercase letter
// (e.g. QRCode -> qrCode). This is a bit hacky, but it works for the current set of names and avoids the
// need for a manual mapping. This ensures that the generated Swift code follows typical naming patterns.
for (int i = 2; i < ret.size() && (std::isupper(ret[i]) || std::isdigit(ret[i])); ++i)
ret[i - 1] = std::tolower(ret[i - 1]);
ret.back() = std::tolower(ret.back());
return ret == "eanupc" ? "eanUPC" : ret == "upca" ? "upcA" : ret == "upce" ? "upcE" : ret;
};
PrintBFs(" public static let {:15} = BarcodeFormat(rawValue: 0x{:04X})", swiftName);
} else if (is("Android")) {
auto androidName = [](const char* name) {
std::string sv(name);
std::string ret = std::string(1, (char)std::toupper(sv[0]));
for (size_t i = 1; i < sv.size(); ++i) {
const int c = sv[i];
const int prev = sv[i - 1];
const int next = i + 1 < sv.size() ? sv[i + 1] : 0;
if ((std::isdigit(c) != std::isdigit(prev)) || (std::isupper(c) && std::islower(prev))
|| (std::isupper(c) && std::isupper(prev) && next != 0 && std::islower(next)))
ret.push_back('_');
ret.push_back(static_cast<char>(std::toupper(c)));
}
return ret == "EANUPC" ? "EAN_UPC"
: ret == "UPCA" ? "UPC_A"
: ret == "UPCE" ? "UPC_E"
: ret == "ALL_GS_1" ? "ALL_GS1"
: ret;
};
PrintBFs("\t\t{:20}(0x{:04X}),", androidName);
} else {
PrintUsage(argv[0]);
return -1;
}
return 0;
}
#else
#include <cstdio>
int main()
{
printf("This tool requires C++20 support with std::format. Please use a compatible compiler.\n");
return -1;
}
#endif

31
tools/chatgui/zxing-cpp/example/ZXingOpenCV.cpp

@ -0,0 +1,31 @@
/*
* Copyright 2021 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "ZXingOpenCV.h"
#include <iostream>
using namespace cv;
int main()
{
namedWindow("Display window");
Mat image;
VideoCapture cap(0);
if (!cap.isOpened())
std::cout << "cannot open camera";
else
while (waitKey(25) != 27) {
cap >> image;
auto barcodes = ReadBarcodes(image);
for (auto& barcode : barcodes)
DrawBarcode(image, barcode);
imshow("Display window", image);
}
return 0;
}

45
tools/chatgui/zxing-cpp/example/ZXingOpenCV.h

@ -0,0 +1,45 @@
/*
* Copyright 2020 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "ReadBarcode.h"
#include <opencv2/opencv.hpp>
inline ZXing::ImageView ImageViewFromMat(const cv::Mat& image)
{
using ZXing::ImageFormat;
auto fmt = ImageFormat::None;
switch (image.channels()) {
case 1: fmt = ImageFormat::Lum; break;
case 2: fmt = ImageFormat::LumA; break;
case 3: fmt = ImageFormat::BGR; break;
case 4: fmt = ImageFormat::BGRA; break;
}
if (image.depth() != CV_8U || fmt == ImageFormat::None)
return {nullptr, 0, 0, ImageFormat::None};
return {image.data, image.cols, image.rows, fmt};
}
inline ZXing::Barcodes ReadBarcodes(const cv::Mat& image, const ZXing::ReaderOptions& options = {})
{
return ZXing::ReadBarcodes(ImageViewFromMat(image), options);
}
inline void DrawBarcode(cv::Mat& img, ZXing::Barcode barcode)
{
auto pos = barcode.position();
auto zx2cv = [](ZXing::PointI p) { return cv::Point(p.x, p.y); };
auto contour = std::vector<cv::Point>{zx2cv(pos[0]), zx2cv(pos[1]), zx2cv(pos[2]), zx2cv(pos[3])};
const auto* pts = contour.data();
int npts = contour.size();
cv::polylines(img, &pts, &npts, 1, true, CV_RGB(0, 255, 0));
cv::putText(img, barcode.text(), zx2cv(pos[3]) + cv::Point(0, 20), cv::FONT_HERSHEY_DUPLEX, 0.5, CV_RGB(0, 255, 0));
}

392
tools/chatgui/zxing-cpp/example/ZXingReader.cpp

@ -0,0 +1,392 @@
/*
* Copyright 2016 Nu-book Inc.
* Copyright 2019 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "GTIN.h"
#include "ZXingCpp.h"
#include <chrono>
#include <cctype>
#include <cstring>
#include <iostream>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <memory>
#include <string>
#include <vector>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
#ifdef ZXING_USE_WEBP
#include <webp/decode.h>
#endif
using namespace ZXing;
struct CLI
{
std::vector<std::string> filePaths;
std::string outPath;
int forceChannels = 0;
int rotate = 0;
bool oneLine = false;
bool bytesOnly = false;
bool showSymbol = false;
bool json = false;
};
static void PrintUsage(const char* exePath)
{
std::cout << "Usage: " << exePath << " [options] <image file>...\n"
<< " -fast Skip some lines/pixels during detection (faster)\n"
<< " -norotate Don't try rotated image during detection (faster)\n"
<< " -noinvert Don't search for inverted codes during detection (faster)\n"
<< " -noscale Don't try downscaled images during detection (faster)\n"
<< " -formats <FORMAT[,...]>\n"
<< " Only detect given format(s) (faster)\n"
<< " -single Stop after the first barcode is detected (faster)\n"
<< " -ispure Assume the image contains only a 'pure'/perfect code (faster)\n"
<< " -errors Include barcodes with errors (like checksum error)\n"
<< " -binarizer <local|global|fixed>\n"
<< " Binarizer to be used for gray to binary conversion\n"
<< " -mode <plain|eci|hri|escaped>\n"
<< " Text mode used to render the raw byte content into text\n"
<< " -1 Print only file name, content/error on one line per file/barcode (implies '-mode Escaped')\n"
<< " -symbol Print the detected symbol (if available)\n"
<< " -json Print a complete JSON formatted serialization\n"
#ifdef ZXING_EXPERIMENTAL_API
<< " -denoise Use extra denoiseing (closing operation)\n"
#endif
<< " -bytes Write (only) the bytes content of the symbol(s) to stdout\n"
<< " -pngout <file name>\n"
<< " Write a copy of the input image with barcodes outlined by a green line\n"
<< " -help Print usage information\n"
<< " -version Print version information\n"
<< "\n"
<< "Supported formats are (Symbology : Variants):";
for (auto f : BarcodeFormats::list(BarcodeFormat::AllReadable)) {
if (Symbology(f) == f)
std::cout << "\n " << std::setw(13) << ToString(f) << " : ";
else
std::cout << ToString(f) << ", ";
}
std::cout << "\n\n";
std::cout << "BarcodeFormats can be lowercase, with or without any of ' -_/', separated by ',' or '|'\n";
}
static bool ParseOptions(int argc, char* argv[], ReaderOptions& options, CLI& cli)
{
for (int i = 1; i < argc; ++i) {
auto is = [&](const char* str) { return strlen(argv[i]) > 1 && strncmp(argv[i], str, strlen(argv[i])) == 0; };
if (is("-fast")) {
options.tryHarder(false);
} else if (is("-norotate")) {
options.tryRotate(false);
} else if (is("-noinvert")) {
options.tryInvert(false);
} else if (is("-noscale")) {
options.tryDownscale(false);
#ifdef ZXING_EXPERIMENTAL_API
} else if (is("-denoise")) {
options.tryDenoise(true);
#endif
} else if (is("-single")) {
options.maxNumberOfSymbols(1);
} else if (is("-ispure")) {
options.isPure(true);
options.binarizer(Binarizer::FixedThreshold);
} else if (is("-errors")) {
options.returnErrors(true);
} else if (is("-formats")) {
if (++i == argc)
return false;
try {
options.formats(BarcodeFormatsFromString(argv[i]));
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n\n";
return false;
}
} else if (is("-binarizer")) {
if (++i == argc)
return false;
else if (is("local"))
options.binarizer(Binarizer::LocalAverage);
else if (is("global"))
options.binarizer(Binarizer::GlobalHistogram);
else if (is("fixed"))
options.binarizer(Binarizer::FixedThreshold);
else
return false;
} else if (is("-mode")) {
if (++i == argc)
return false;
else if (is("plain"))
options.textMode(TextMode::Plain);
else if (is("eci"))
options.textMode(TextMode::ECI);
else if (is("hri"))
options.textMode(TextMode::HRI);
else if (is("escaped"))
options.textMode(TextMode::Escaped);
else
return false;
} else if (is("-1")) {
cli.oneLine = true;
} else if (is("-bytes")) {
cli.bytesOnly = true;
} else if (is("-symbol")) {
cli.showSymbol = true;
} else if (is("-json")) {
cli.json = true;
} else if (is("-pngout")) {
if (++i == argc)
return false;
cli.outPath = argv[i];
} else if (is("-channels")) {
if (++i == argc)
return false;
cli.forceChannels = std::stoi(argv[i]);
} else if (is("-rotate")) {
if (++i == argc)
return false;
cli.rotate = std::stoi(argv[i]);
} else if (is("-help") || is("--help")) {
PrintUsage(argv[0]);
exit(0);
} else if (is("-version") || is("--version")) {
std::cout << "ZXingReader version " << Version() << "\n";
exit(0);
} else {
cli.filePaths.push_back(argv[i]);
}
}
return !cli.filePaths.empty();
}
void drawLine(const ImageView& iv, PointI a, PointI b, bool error)
{
int steps = maxAbsComponent(b - a);
PointF dir = bresenhamDirection(PointF(b - a));
int R = RedIndex(iv.format()), G = GreenIndex(iv.format()), B = BlueIndex(iv.format());
for (int i = 0; i < steps; ++i) {
auto p = PointI(centered(a + i * dir));
auto* dst = const_cast<uint8_t*>(iv.data(p.x, p.y));
if (dst < iv.data(0, 0) || dst > iv.data(iv.width() - 1, iv.height() - 1))
continue;
dst[R] = error ? 0xff : 0;
dst[G] = error ? 0 : 0xff;
dst[B] = 0;
}
}
void drawRect(const ImageView& image, const Position& pos, bool error)
{
for (int i = 0; i < 4; ++i)
drawLine(image, pos[i], pos[(i + 1) % 4], error);
}
std::unique_ptr<uint8_t, void (*)(void*)> LoadWebP(const std::string& filePath, int* width, int* height, int* channels)
{
#ifdef ZXING_USE_WEBP
std::ifstream file(filePath, std::ios::binary);
if (!file)
throw std::runtime_error("Failed to read image: " + filePath + " (failed to open file)");
std::vector<uint8_t> compressedData(std::istreambuf_iterator<char>(file), {});
if (compressedData.empty())
throw std::runtime_error("Failed to read image: " + filePath + " (empty file)");
*channels = 3;
uint8_t* img = WebPDecodeRGB(compressedData.data(), compressedData.size(), width, height);
if (img == nullptr)
throw std::runtime_error("Failed to read image: " + filePath + " (WebP decode failed)");
return std::unique_ptr<uint8_t, void (*)(void*)>(img, WebPFree);
#else
std::cerr << "Failed to read image: " << filePath << " (WebP support is not enabled in this build)" << "\n";
return std::unique_ptr<uint8_t, void (*)(void*)>(nullptr, nullptr);
#endif
}
std::unique_ptr<uint8_t, void (*)(void*)> LoadStbi(const std::string& filePath, int* width, int* height, int* channels, int forceChannels)
{
auto buffer = std::unique_ptr<uint8_t, void (*)(void*)>(
filePath == "-" ? stbi_load_from_file(stdin, width, height, channels, forceChannels)
: stbi_load(filePath.c_str(), width, height, channels, forceChannels),
stbi_image_free);
if (buffer == nullptr)
std::cerr << "Failed to read image: " << filePath << " (" << stbi_failure_reason() << ")" << "\n";
return buffer;
}
bool IEndsWith(std::string_view str, std::string_view suffix)
{
return str.size() > suffix.size() && std::equal(str.end() - suffix.size(), str.end(), suffix.begin(),
[](char a, char b) { return std::tolower(static_cast<unsigned char>(a)) == b; });
}
int main(int argc, char* argv[])
{
ReaderOptions options;
CLI cli;
Barcodes allBarcodes;
int ret = 0;
options.textMode(TextMode::HRI);
options.eanAddOnSymbol(EanAddOnSymbol::Read);
if (!ParseOptions(argc, argv, options, cli)) {
PrintUsage(argv[0]);
return -1;
}
std::cout.setf(std::ios::boolalpha);
if (!cli.outPath.empty())
cli.forceChannels = 3; // the drawing code only works for RGB data
for (const auto& filePath : cli.filePaths) {
int width, height, channels;
auto buffer = IEndsWith(filePath, ".webp")
? LoadWebP(filePath, &width, &height, &channels)
: LoadStbi(filePath, &width, &height, &channels, cli.forceChannels);
if (buffer == nullptr)
continue;
channels = cli.forceChannels ? cli.forceChannels : channels;
auto ImageFormatFromChannels = std::array{ImageFormat::None, ImageFormat::Lum, ImageFormat::LumA, ImageFormat::RGB, ImageFormat::RGBA};
ImageView image{buffer.get(), width, height, ImageFormatFromChannels.at(channels)};
auto barcodes = ReadBarcodes(image.rotated(cli.rotate), options);
// if we did not find anything, insert a dummy to produce some output for each file
if (barcodes.empty())
barcodes.emplace_back();
allBarcodes.insert(allBarcodes.end(), barcodes.begin(), barcodes.end());
if (filePath == cli.filePaths.back()) {
auto merged = MergeStructuredAppendSequences(allBarcodes);
// report all merged sequences as part of the last file to make the logic not overly complicated here
barcodes.insert(barcodes.end(), std::make_move_iterator(merged.begin()), std::make_move_iterator(merged.end()));
}
for (auto&& barcode : barcodes) {
if (!cli.outPath.empty())
drawRect(image, barcode.position(), bool(barcode.error()));
ret |= static_cast<int>(barcode.error().type());
if (cli.bytesOnly) {
std::cout.write(reinterpret_cast<const char*>(barcode.bytes().data()), barcode.bytes().size());
continue;
}
if (cli.json) {
if (barcode.format() != BarcodeFormat::None)
std::cout << "{\"FilePath\":\"" << filePath << "\"," << barcode.extra("ALL").substr(1) << "\n";
continue;
}
if (cli.oneLine) {
std::cout << filePath << " " << ToString(barcode.format());
if (barcode.isValid())
std::cout << " \"" << barcode.text(TextMode::Escaped) << "\"";
else if (barcode.error())
std::cout << " " << ToString(barcode.error());
std::cout << "\n";
continue;
}
if (cli.filePaths.size() > 1 || barcodes.size() > 1) {
static bool firstFile = true;
if (!firstFile)
std::cout << "\n";
if (cli.filePaths.size() > 1)
std::cout << "File: " << filePath << "\n";
firstFile = false;
}
if (barcode.format() == BarcodeFormat::None) {
std::cout << "No barcode found\n";
continue;
}
std::cout << "Text: \"" << barcode.text() << "\"\n"
<< "Bytes: " << barcode.text(options.textMode() == TextMode::ECI ? TextMode::HexECI : TextMode::Hex) << "\n"
<< "Format: " << ToString(barcode.format()) << "\n"
<< "Symbology: " << ToString(barcode.symbology()) << "\n"
<< "Identifier: " << barcode.symbologyIdentifier() << "\n"
<< "Content: " << ToString(barcode.contentType()) << "\n"
<< "HasECI: " << barcode.hasECI() << "\n"
<< "Position: " << ToString(barcode.position()) << "\n"
<< "Rotation: " << barcode.orientation() << " deg\n"
<< "IsMirrored: " << barcode.isMirrored() << "\n"
<< "IsInverted: " << barcode.isInverted() << "\n";
auto printOptional = [](const char* key, const std::string& v) {
if (!v.empty())
std::cout << key << v << "\n";
};
printOptional("ECLevel: ", barcode.ecLevel());
printOptional("Version: ", barcode.version());
printOptional("Error: ", ToString(barcode.error()));
if (barcode.lineCount())
std::cout << "Lines: " << barcode.lineCount() << "\n";
if (barcode.symbology() == BarcodeFormat::EANUPC) {
printOptional("Country: ", GTIN::LookupCountryIdentifier(barcode.text(), barcode.format()));
printOptional("Add-On: ", GTIN::EanAddOn(barcode));
printOptional("Price: ", GTIN::Price(GTIN::EanAddOn(barcode)));
printOptional("Issue #: ", GTIN::IssueNr(GTIN::EanAddOn(barcode)));
} else if (barcode.format() == BarcodeFormat::ITF && barcode.bytes().size() == 14) {
printOptional("Country: ", GTIN::LookupCountryIdentifier(barcode.text(), barcode.format()));
}
if (barcode.isPartOfSequence())
std::cout << "Structured Append: symbol " << barcode.sequenceIndex() + 1 << " of "
<< barcode.sequenceSize() << " (parity/id: '" << barcode.sequenceId() << "')\n";
else if (barcode.sequenceSize() > 0)
std::cout << "Structured Append: merged result from " << barcode.sequenceSize() << " symbols (parity/id: '"
<< barcode.sequenceId() << "')\n";
printOptional("Extra: ", barcode.extra());
if (cli.showSymbol && barcode.symbol().data())
std::cout << "Symbol:\n" << WriteBarcodeToUtf8(barcode);
}
if (cli.filePaths.size() == 1 && !cli.outPath.empty())
stbi_write_png(cli.outPath.c_str(), image.width(), image.height(), 3, image.data(), image.rowStride());
#ifdef NDEBUG
if (getenv("MEASURE_PERF")) {
auto startTime = std::chrono::high_resolution_clock::now();
auto duration = startTime - startTime;
int N = 0;
int blockSize = 1;
do {
for (int i = 0; i < blockSize; ++i)
ReadBarcodes(image, options);
N += blockSize;
duration = std::chrono::high_resolution_clock::now() - startTime;
if (blockSize < 1000 && duration < std::chrono::milliseconds(100))
blockSize *= 10;
} while (duration < std::chrono::seconds(1));
fprintf(stderr, "time: %5.2f ms per frame\n", double(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()) / N);
}
#endif
}
return ret;
}

235
tools/chatgui/zxing-cpp/example/ZXingWriter.cpp

@ -0,0 +1,235 @@
/*
* Copyright 2016 Nu-book Inc.
*/
// SPDX-License-Identifier: Apache-2.0
// #define USE_OLD_WRITER_API
#ifndef USE_OLD_WRITER_API
#include "CreateBarcode.h"
#include "WriteBarcode.h"
#else
#include "BitMatrix.h"
#include "BitMatrixIO.h"
#include "CharacterSet.h"
#include "MultiFormatWriter.h"
#endif
#include "Version.h"
#include <algorithm>
#include <cctype>
#include <cstring>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <string>
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
using namespace ZXing;
static void PrintUsage(const char* exePath)
{
std::cout << "Usage: " << exePath
<< " [-options <creator-options>] [-scale <factor>] [-binary] [-noqz] [-hrt] [-invert] <format> <text> <output>\n"
<< " -options Comma separated list of format specific options and flags\n"
<< " -scale module size of generated image / negative numbers mean 'target size in pixels'\n"
<< " -rotate Rotate image by given angle (90, 180 or 270)\n"
// << " -encoding Encoding used to encode input text\n"
<< " -binary Interpret <text> as a file name containing binary data\n"
<< " -noqz Print barcode without quiet zone\n"
<< " -hrt Print human readable text below the barcode (if supported)\n"
<< " -invert Invert colors (switch black and white)\n"
<< " -help Print usage information\n"
<< " -version Print version information\n"
<< "\n"
<< "Supported formats are (Symbology : Variants):";
for (auto f : BarcodeFormats::list(BarcodeFormat::AllCreatable)) {
if (Symbology(f) == f || f == BarcodeFormat::DXFilmEdge)
std::cout << "\n " << std::setw(13) << ToString(f) << " : ";
else
std::cout << ToString(f) << ", ";
}
std::cout << "\n\n";
std::cout << "Format can be lowercase letters, with or without any of ' -_/'.\n"
<< "Output format is determined by file name, supported are png, jpg and svg.\n";
}
struct CLI
{
BarcodeFormat format = BarcodeFormat::None;
int scale = 0;
int rotate = 0;
std::string input;
std::string outPath;
std::string options;
bool inputIsFile = false;
bool invert = false;
bool addHRT = false;
bool addQZs = true;
bool verbose = false;
// CharacterSet encoding = CharacterSet::Unknown;
};
static bool ParseOptions(int argc, char* argv[], CLI& cli)
{
int nonOptArgCount = 0;
for (int i = 1; i < argc; ++i) {
auto is = [&](const char* str) { return strncmp(argv[i], str, strlen(argv[i])) == 0; };
if (is("-scale")) {
if (++i == argc)
return false;
cli.scale = std::stoi(argv[i]);
} else if (is("-rotate")) {
if (++i == argc)
return false;
cli.rotate = std::stoi(argv[i]);
// } else if (is("-encoding")) {
// if (++i == argc)
// return false;
// cli.encoding = CharacterSetFromString(argv[i]);
} else if (is("-binary")) {
cli.inputIsFile = true;
} else if (is("-hrt")) {
cli.addHRT = true;
} else if (is("-noqz")) {
cli.addQZs = false;
} else if (is("-invert")) {
cli.invert = true;
} else if (is("-options")) {
if (++i == argc)
return false;
cli.options = argv[i];
} else if (is("-verbose")) {
cli.verbose = true;
} else if (is("-help") || is("--help")) {
PrintUsage(argv[0]);
exit(0);
} else if (is("-version") || is("--version")) {
std::cout << "ZXingWriter version " << ZXING_VERSION_STR << " (with "
#ifdef ZXING_USE_ZINT
<< "zint"
#else
<< "zxing-cpp"
#endif
<< " backend) \n";
exit(0);
} else if (nonOptArgCount == 0) {
try {
cli.format = BarcodeFormatFromString(argv[i]);
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n\n";
return false;
}
++nonOptArgCount;
} else if (nonOptArgCount == 1) {
cli.input = argv[i];
++nonOptArgCount;
} else if (nonOptArgCount == 2) {
cli.outPath = argv[i];
++nonOptArgCount;
} else {
return false;
}
}
return nonOptArgCount == 3;
}
static std::string GetExtension(const std::string& path)
{
auto fileNameStart = path.find_last_of("/\\");
auto fileName = fileNameStart == std::string::npos ? path : path.substr(fileNameStart + 1);
auto extStart = fileName.find_last_of('.');
auto ext = extStart == std::string::npos ? "" : fileName.substr(extStart + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), [](char c) { return std::tolower(c); });
return ext;
}
template <typename T = char>
std::vector<T> ReadFile(const std::string& fn)
{
std::basic_ifstream<T> ifs(fn, std::ios::binary);
if (!ifs.good())
throw std::runtime_error("failed to open/read file " + fn);
return ifs ? std::vector(std::istreambuf_iterator<T>(ifs), std::istreambuf_iterator<T>()) : std::vector<T>();
};
int main(int argc, char* argv[])
{
CLI cli;
if (!ParseOptions(argc, argv, cli)) {
PrintUsage(argv[0]);
return -1;
}
try {
#if 1
auto cOpts = CreatorOptions(cli.format, cli.options);
auto barcode = cli.inputIsFile ? CreateBarcodeFromBytes(ReadFile(cli.input), cOpts) : CreateBarcodeFromText(cli.input, cOpts);
auto wOpts = WriterOptions().scale(cli.scale).addQuietZones(cli.addQZs).addHRT(cli.addHRT).invert(cli.invert).rotate(cli.rotate);
auto bitmap = WriteBarcodeToImage(barcode, wOpts);
if (cli.verbose) {
std::cout.setf(std::ios::boolalpha);
std::cout << "Text: \"" << barcode.text() << "\"\n"
<< "Bytes: " << barcode.text(TextMode::Hex) << "\n"
<< "Format: " << ToString(barcode.format()) << "\n"
<< "Symbology: " << ToString(barcode.symbology()) << "\n"
<< "Identifier: " << barcode.symbologyIdentifier() << "\n"
<< "Content: " << ToString(barcode.contentType()) << "\n"
<< "HasECI: " << barcode.hasECI() << "\n"
<< "Position: " << ToString(barcode.position()) << "\n"
<< "Rotation: " << barcode.orientation() << " deg\n"
<< "IsMirrored: " << barcode.isMirrored() << "\n"
<< "IsInverted: " << barcode.isInverted() << "\n"
<< "ECLevel: " << barcode.ecLevel() << "\n";
std::cout << WriteBarcodeToUtf8(barcode, wOpts);
}
#else // 'old' writer API (non zint based)
auto writer = MultiFormatWriter(cli.format).setMargin(cli.addQZs ? 10 : 0);
BitMatrix matrix;
if (cli.inputIsFile) {
auto file = ReadFile(cli.input);
std::wstring bytes;
for (uint8_t c : file)
bytes.push_back(c);
writer.setEncoding(CharacterSet::BINARY);
matrix = writer.encode(bytes, cli.scale, std::clamp(cli.scale / 2, 50, 300));
} else {
writer.setEncoding(CharacterSet::UTF8);
matrix = writer.encode(cli.input, cli.scale, std::clamp(cli.scale / 2, 50, 300));
}
auto bitmap = ToMatrix<uint8_t>(matrix);
#endif
auto ext = GetExtension(cli.outPath);
int success = 0;
if (ext == "" || ext == "png") {
success = stbi_write_png(cli.outPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0);
} else if (ext == "jpg" || ext == "jpeg") {
success = stbi_write_jpg(cli.outPath.c_str(), bitmap.width(), bitmap.height(), 1, bitmap.data(), 0);
} else if (ext == "svg") {
#ifndef USE_OLD_WRITER_API
success = (std::ofstream(cli.outPath) << WriteBarcodeToSVG(barcode, wOpts)).good();
#else
success = (std::ofstream(cli.outPath) << ToSVG(matrix)).good();
#endif
}
if (!success) {
std::cerr << "Failed to write image: " << cli.outPath << "\n";
return -1;
}
} catch (const std::exception& e) {
std::cerr << e.what() << "\n";
return -1;
}
return 0;
}

752
tools/chatgui/zxing-cpp/test/blackbox/BlackboxTestRunner.cpp

@ -0,0 +1,752 @@
/*
* Copyright 2016 Nu-book Inc.
* Copyright 2019 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "BlackboxTestRunner.h"
#include "ByteArray.h"
#include "ImageLoader.h"
#include "ReadBarcode.h"
#include "Utf.h"
#include "ZXAlgorithms.h"
#include "StdPrint.h"
#include "Version.h"
#include <chrono>
#include <exception>
#include <format>
#include <fstream>
#include <map>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#ifdef __GNUC__
// turns out that libstdc++ is depending on TBB without even linking against it, so this fails to link :-/
#undef __cpp_lib_execution
#endif
#ifdef __cpp_lib_execution
#include <execution>
#else
#include <future>
#endif
namespace ZXing::Test {
namespace {
struct PureTag {} pure;
struct TestCase
{
struct TC
{
std::string name = {};
int minPassCount = 0; // The number of images which must decode for the test to pass.
int maxMisreads = 0; // Maximum number of successfully read images with the wrong contents.
std::set<fs::path> notDetectedFiles = {};
std::map<fs::path, std::string> misReadFiles = {};
};
TC tc[2] = {};
int rotation = 0; // The rotation in degrees clockwise to use for this test.
TestCase(int mntf, int mnts, int mmf, int mms, int r) : tc{{"fast", mntf, mmf}, {"slow", mnts, mms}}, rotation(r) {}
TestCase(int mntf, int mnts, int r) : TestCase(mntf, mnts, 0, 0, r) {}
TestCase(int mntp, int mmp, PureTag) : tc{{"pure", mntp, mmp}} {}
};
struct FalsePositiveTestCase
{
int maxAllowed; // Maximum number of images which can fail due to successfully reading the wrong contents
int rotation; // The rotation in degrees clockwise to use for this test.
};
}
// Helper for `compareResult()` - map `key` to Barcode property, converting value to std::string
static std::string getBarcodeValue(const Barcode& barcode, const std::string& key)
{
if (key == "format")
return ToString(barcode.format());
if (key == "contentType")
return ToString(barcode.contentType());
if (key == "ecLevel")
return barcode.ecLevel();
if (key == "orientation")
return std::to_string(barcode.orientation());
if (key == "symbologyIdentifier")
return barcode.symbologyIdentifier();
if (key == "sequenceSize")
return std::to_string(barcode.sequenceSize());
if (key == "sequenceIndex")
return std::to_string(barcode.sequenceIndex());
if (key == "sequenceId")
return barcode.sequenceId();
if (key == "isLastInSequence")
return barcode.isLastInSequence() ? "true" : "false";
if (key == "isPartOfSequence")
return barcode.isPartOfSequence() ? "true" : "false";
if (key == "isMirrored")
return barcode.isMirrored() ? "true" : "false";
if (key == "isInverted")
return barcode.isInverted() ? "true" : "false";
if (key == "readerInit")
return barcode.readerInit() ? "true" : "false";
return std::format("***Unknown key '{}'***", key);
}
// Read ".result.txt" file contents `expected` with lines "key=value" and compare to `actual`
static bool compareResult(const Barcode& barcode, const std::string& expected, std::string& actual)
{
bool ret = true;
actual.clear();
actual.reserve(expected.size());
std::stringstream expectedLines(expected);
std::string expectedLine;
while (std::getline(expectedLines, expectedLine)) {
if (expectedLine.empty() || expectedLine[0] == '#')
continue;
auto equals = expectedLine.find('=');
if (equals == std::string::npos) {
actual += "***Bad format, missing equals***\n";
return false;
}
std::string key = expectedLine.substr(0, equals);
std::string expectedValue = expectedLine.substr(equals + 1);
std::string actualValue = getBarcodeValue(barcode, key);
if (actualValue != expectedValue) {
ret = false;
actualValue += " ***Mismatch***";
}
actual += StrCat(key, '=', actualValue, '\n');
}
return ret;
}
static std::string checkResult(const fs::path& imgPath, BarcodeFormat expectedFormat, const Barcode& barcode)
{
if (expectedFormat != barcode.format() && expectedFormat != barcode.symbology()) // allow matching either format or symbology
return std::format("Format mismatch: expected '{}' but got '{}'", ToString(expectedFormat), ToString(barcode.format()));
auto readFile = [imgPath](const char* ending) {
std::ifstream ifs(fs::path(imgPath).replace_extension(ending), std::ios::binary);
return ifs ? std::optional(std::string(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>())) : std::nullopt;
};
if (auto expected = readFile(".result.txt")) {
std::string actual;
if (!compareResult(barcode, *expected, actual))
return std::format("Result mismatch: expected\n{} but got\n{}", *expected, actual);
}
if (auto expected = readFile(".txt")) {
expected = EscapeNonGraphical(*expected);
auto utf8Result = barcode.text(TextMode::Escaped);
return utf8Result != *expected ? std::format("Content mismatch: expected '{}' but got '{}'", *expected, utf8Result) : "";
}
if (auto expected = readFile(".bin")) {
ByteArray binaryExpected(*expected);
return barcode.bytes() != binaryExpected
? std::format("Content mismatch: expected '{}' but got '{}'", ToHex(binaryExpected), ToHex(barcode.bytes()))
: "";
}
return "Error reading file";
}
static int failed = 0;
static int extra = 0;
static int totalImageLoadTime = 0;
int timeSince(std::chrono::steady_clock::time_point startTime)
{
auto duration = std::chrono::steady_clock::now() - startTime;
return narrow_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count());
}
// pre-load images into cache, so the disc io time does not end up in the timing measurement
void preloadImageCache(const std::vector<fs::path>& imgPaths)
{
auto startTime = std::chrono::steady_clock::now();
ImageLoader::clearCache();
for (const auto& imgPath : imgPaths)
ImageLoader::load(imgPath);
totalImageLoadTime += timeSince(startTime);
}
static std::string printPositiveTestStats(int imageCount, const TestCase::TC& tc)
{
int passCount = imageCount - Size(tc.misReadFiles) - Size(tc.notDetectedFiles);
std::print(" | {}: {:3} of {:3}, misread {} of {}", tc.name, passCount, tc.minPassCount, Size(tc.misReadFiles), tc.maxMisreads);
std::string failures;
if (passCount < tc.minPassCount && !tc.notDetectedFiles.empty()) {
failures += std::format(" Not detected ({}):", tc.name);
for (const auto& f : tc.notDetectedFiles)
failures += std::format(" {}", f.filename().string());
failures += "\n";
failed += tc.minPassCount - passCount;
}
extra += std::max(0, passCount - tc.minPassCount);
if (passCount > tc.minPassCount)
failures += std::format(" Unexpected detections ({}): {}\n", tc.name, passCount - tc.minPassCount);
if (Size(tc.misReadFiles) > tc.maxMisreads) {
failures += std::format(" Read error ({}):", tc.name);
for (const auto& [path, error] : tc.misReadFiles)
failures += std::format(" {}: {}\n", path.filename().string(), error);
failed += Size(tc.misReadFiles) - tc.maxMisreads;
}
return failures;
}
static std::vector<fs::path> getImagesInDirectory(const fs::path& directory)
{
std::vector<fs::path> result;
for (const auto& entry : fs::directory_iterator(directory))
if (fs::is_regular_file(entry.status()) && Contains({".webp", ".png", ".jpg", ".pgm", ".gif"}, entry.path().extension()))
result.push_back(entry.path());
preloadImageCache(result);
return result;
}
static void doRunTests(const fs::path& directory, BarcodeFormat format, int totalTests, const std::vector<TestCase>& tests,
ReaderOptions opts)
{
auto imgPaths = getImagesInDirectory(directory);
auto folderName = directory.stem();
if (Size(imgPaths) != totalTests)
std::println("TEST {} => Expected number of tests: {}, got: {} => FAILED", folderName.string(), totalTests, imgPaths.size());
for (auto& test : tests) {
std::print("{:20} @ {:3}, {:3}", folderName.string(), test.rotation, Size(imgPaths));
std::vector<int> times;
std::string failures;
for (auto tc : test.tc) {
if (tc.name.empty())
break;
auto startTime = std::chrono::steady_clock::now();
opts.tryDownscale(tc.name == "slow_");
opts.downscaleFactor(2);
opts.downscaleThreshold(180);
opts.tryHarder(tc.name == "slow");
opts.tryRotate(tc.name == "slow");
opts.tryInvert(tc.name == "slow");
opts.isPure(tc.name == "pure");
if (opts.isPure())
opts.binarizer(Binarizer::FixedThreshold);
#ifndef PRINT_DEBUG
#ifdef __cpp_lib_execution
std::vector<Barcode> barcodes(imgPaths.size());
std::transform(std::execution::par, imgPaths.begin(), imgPaths.end(), barcodes.begin(), [&](const fs::path& imgPath) {
return ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts);
});
for (size_t i = 0; i < imgPaths.size(); ++i) {
const auto& imgPath = imgPaths[i];
const auto& barcode = barcodes[i];
#else
auto futures = std::vector<std::pair<fs::path, std::future<Barcode>>>{};
for (const auto& imgPath : imgPaths) {
futures.push_back(std::make_pair(imgPath, std::async(std::launch::async, [&](const fs::path& path) {
return ReadBarcode(ImageLoader::load(path).rotated(test.rotation), opts);
}, imgPath)));
}
for (auto& [imgPath, fut] : futures) {
auto barcode = fut.get();
#endif // __cpp_lib_execution
#else
for (const auto& imgPath : imgPaths) {
auto barcode = ReadBarcode(ImageLoader::load(imgPath).rotated(test.rotation), opts);
#endif
if (barcode.isValid()) {
auto error = checkResult(imgPath, format, barcode);
if (!error.empty())
tc.misReadFiles[imgPath] = error;
} else {
tc.notDetectedFiles.insert(imgPath);
}
}
times.push_back(timeSince(startTime));
failures += printPositiveTestStats(Size(imgPaths), tc);
}
std::println(" | time: {:3} vs {:3} ms", times.front(), times.back());
if (!failures.empty())
std::println("\n{}", failures);
}
}
static Barcode readMultiple(const std::vector<fs::path>& imgPaths, BarcodeFormat format)
{
Barcodes allBarcodes;
for (const auto& imgPath : imgPaths) {
auto barcodes = ReadBarcodes(ImageLoader::load(imgPath),
ReaderOptions().formats(format).tryDownscale(false));
allBarcodes.insert(allBarcodes.end(), barcodes.begin(), barcodes.end());
}
return MergeStructuredAppendSequence(allBarcodes);
}
static void doRunStructuredAppendTest(const fs::path& directory, BarcodeFormat format, int totalTests,
const std::vector<TestCase>& tests)
{
auto imgPaths = getImagesInDirectory(directory);
auto folderName = directory.stem();
std::map<fs::path, std::vector<fs::path>> imageGroups;
for (const auto& imgPath : imgPaths) {
std::string fn = imgPath.filename().string();
auto p = fn.find_last_of('-');
imageGroups[imgPath.parent_path() / fn.substr(0, p)].push_back(imgPath);
}
if (Size(imageGroups) != totalTests)
std::println("TEST {} => Expected number of tests: {}, got: {} => FAILED", folderName.string(), totalTests,
imageGroups.size());
for (auto& test : tests) {
std::print("{:20} @ {:3}, {:3}", folderName.string(), test.rotation, Size(imgPaths));
auto tc = test.tc[0];
auto startTime = std::chrono::steady_clock::now();
for (const auto& [testPath, testImgPaths] : imageGroups) {
auto barcode = readMultiple(testImgPaths, format);
if (barcode.isValid()) {
auto error = checkResult(testPath, format, barcode);
if (!error.empty())
tc.misReadFiles[testPath] = error;
} else {
tc.notDetectedFiles.insert(testPath);
}
}
auto failures = printPositiveTestStats(Size(imageGroups), tc);
std::println(" | time: {:3} ms", timeSince(startTime));
if (!failures.empty())
std::println("\n{}", failures);
}
}
int runBlackBoxTests(const fs::path& testPathPrefix, const std::set<std::string>& includedTests)
{
auto hasTest = [&includedTests](const fs::path& dir) {
auto stem = dir.stem().string();
return includedTests.empty() || Contains(includedTests, stem) ||
Contains(includedTests, stem.substr(0, stem.size() - 2));
};
auto runTests = [&](std::string_view directory, BarcodeFormat format, int total,
const std::vector<TestCase>& tests, const ReaderOptions& opts = ReaderOptions()) {
if (hasTest(directory) && format & (BarcodeFormat::AllReadable | BarcodeFormat::None))
doRunTests(testPathPrefix / directory, format, total, tests, opts);
};
auto runStructuredAppendTest = [&](std::string_view directory, BarcodeFormat format, int total,
const std::vector<TestCase>& tests) {
if (hasTest(directory) && format & (BarcodeFormat::AllReadable | BarcodeFormat::None))
doRunStructuredAppendTest(testPathPrefix / directory, format, total, tests);
};
try
{
auto startTime = std::chrono::steady_clock::now();
using enum BarcodeFormat;
// clang-format off
// Expected failures:
// abc-inverted.png (fast) - fast does not try inverted
// az-thick.png (pure)
runTests("aztec-1", Aztec, 32, {
{ 31, 32, 0 },
{ 31, 32, 90 },
{ 31, 32, 180 },
{ 31, 32, 270 },
{ 30, 0, pure },
});
runTests("aztec-2", Aztec, 16, {
{ 16, 16, 0 },
{ 16, 16, 90 },
{ 16, 16, 180 },
{ 16, 16, 270 },
});
runTests("datamatrix-1", DataMatrix, 30, {
{ 30, 30, 0 },
{ 0, 28, 90 },
{ 0, 28, 180 },
{ 0, 28, 270 },
{ 28, 0, pure },
});
runTests("datamatrix-2", DataMatrix, 13, {
{ 13, 13, 0 },
{ 0, 13, 90 },
{ 0, 13, 180 },
{ 0, 13, 270 },
});
runTests("datamatrix-3", DataMatrix, 28, {
{ 26, 27, 0 },
{ 0, 27, 90 },
{ 0, 27, 180 },
{ 0, 27, 270 },
});
runTests("datamatrix-4", DataMatrix, 21, {
{ 21, 21, 0 },
{ 0, 21, 90 },
{ 0, 21, 180 },
{ 0, 21, 270 },
{ 19, 0, pure },
});
runTests("dxfilmedge-1", DXFilmEdge, 3, {
{ 1, 3, 0 },
{ 0, 3, 180 },
});
runTests("codabar-1", Codabar, 11, {
{ 11, 11, 0 },
{ 11, 11, 180 },
});
runTests("codabar-2", Codabar, 4, {
{ 3, 4, 0 },
{ 3, 4, 180 },
});
runTests("code39-1", Code39, 6, {
{ 6, 6, 0 },
{ 6, 6, 180 },
});
runTests("code39-2", Code39Ext, 3, {
{ 3, 3, 0 },
{ 3, 3, 180 },
});
runTests("code39-3", Code39, 5, {
{ 5, 5, 0 },
{ 5, 5, 180 },
});
runTests("code93-1", Code93, 3, {
{ 3, 3, 0 },
{ 3, 3, 180 },
});
runTests("code128-1", Code128, 6, {
{ 6, 6, 0 },
{ 6, 6, 180 },
});
runTests("code128-2", Code128, 14, {
{ 13, 14, 0 },
{ 13, 14, 180 },
});
runTests("code128-3", Code128, 2, {
{ 2, 2, 0 },
{ 2, 2, 180 },
});
runTests("ean8-1", EAN8, 9, {
{ 9, 9, 0 },
{ 9, 9, 180 },
{ 8, 0, pure },
});
runTests("ean13-1", EAN13, 16, {
{ 10, 14, 0 },
{ 11, 14, 180 },
});
runTests("ean13-2", EAN13, 16, {
{ 1, 6, 0 },
{ 1, 6, 180 },
});
runTests("ean13-3", EAN13, 8, {
{ 8, 8, 0 },
{ 8, 8, 180 },
});
runTests("ean13-4", EAN13, 20, {
{ 8, 12, 0 },
{ 7, 12, 180 },
});
runTests("ean13-extension-1", EAN13, 5, {
{ 4, 5, 0 },
{ 4, 5, 180 },
}, ReaderOptions().eanAddOnSymbol(EanAddOnSymbol::Require));
runTests("itf-1", ITF, 14, {
{ 13, 14, 0 },
{ 13, 14, 180 },
});
runTests("itf-2", ITF, 6, {
{ 6, 6, 0 },
{ 6, 6, 180 },
});
runTests("telepen-1", Telepen, 4, {
{ 4, 4, 0 },
{ 4, 4, 180 },
});
runTests("upca-1", UPCA, 5, {
{ 4, 5, 0 },
{ 4, 5, 180 },
}, ReaderOptions().formats(UPCA));
runTests("upca-2", UPCA, 26, {
{ 10, 16, 0 },
{ 12, 16, 180 },
}, ReaderOptions().formats(UPCA));
runTests("upca-3", UPCA, 6, {
{ 3, 6, 0 },
{ 3, 6, 180 },
}, ReaderOptions().formats(UPCA));
runTests("upca-4", UPCA, 13, {
{ 5, 9, 0 },
{ 6, 9, 180 },
}, ReaderOptions().formats(UPCA));
runTests("upca-5", UPCA, 20, {
{ 7, 9, 0 },
{ 7, 9, 180 },
}, ReaderOptions().formats(UPCA));
runTests("upca-extension-1", UPCA, 6, {
{ 4, 5, 0 },
{ 4, 5, 180 },
}, ReaderOptions().eanAddOnSymbol(EanAddOnSymbol::Require).formats(UPCA));
runTests("upce-1", UPCE, 3, {
{ 3, 3, 0 },
{ 3, 3, 180 },
{ 3, 0, pure },
});
runTests("upce-2", UPCE, 15, {
{ 9, 10, 0 },
{ 9, 10, 180 },
});
runTests("upce-3", UPCE, 11, {
{ 5, 7, 0 },
{ 5, 7, 180 },
});
runTests("rss14-1", DataBarOmni, 13, {
{ 11, 11, 0 },
{ 11, 11, 180 },
});
runTests("rss14-2", DataBarStk, 7, {
{ 5, 6, 0 },
{ 6, 6, 180 },
});
runTests("rssexpanded-1", DataBarExp, 37, {
{ 37, 37, 0 },
{ 37, 37, 180 },
{ 37, 0, pure },
});
runTests("rssexpanded-2", DataBarExp, 7, {
{ 5, 7, 0 },
{ 5, 7, 180 },
});
runTests("rssexpanded-3", DataBarExp, 118, {
{ 118, 118, 0 },
{ 118, 118, 180 },
{ 118, 0, pure },
});
runTests("rssexpandedstacked-1", DataBarExpStk, 62, {
{ 52, 62, 0 },
{ 52, 62, 180 },
{ 57, 0, pure },
});
runTests("rssexpandedstacked-2", DataBarExpStk, 2, {
{ 2, 2, 0 },
{ 2, 2, 180 },
});
runTests("databarltd-1", DataBarLtd, 2, {
{ 2, 2, 0 },
{ 2, 2, 180 },
{ 1, 0, pure },
});
runTests("maxicode-1", MaxiCode, 9, {
{ 9, 9, 0 },
});
runTests("maxicode-2", MaxiCode, 4, {
{ 0, 0, 0 },
});
runTests("qrcode-1", QRCode, 8, {
{ 8, 8, 0 },
{ 8, 8, 90 },
{ 8, 8, 180 },
{ 8, 8, 270 },
});
runTests("qrcode-2", QRCode, 54, {
{ 48, 51, 0 },
{ 49, 52, 90 },
{ 48, 51, 180 },
{ 48, 51, 270 },
{ 23, 1, pure }, // the misread is the 'outer' symbol in 16.png
});
runTests("qrcode-3", QRCode, 18, {
{ 18, 18, 0 },
{ 18, 18, 90 },
{ 18, 18, 180 },
{ 18, 18, 270 },
});
runTests("qrcode-4", QRCode, 24, {
{ 15, 15, 0 },
{ 15, 15, 90 },
{ 15, 15, 180 },
{ 15, 15, 270 },
});
runTests("qrcode-5", QRCode, 16, {
{ 16, 16, 0 },
{ 16, 16, 90 },
{ 16, 16, 180 },
{ 16, 16, 270 },
{ 4, 0, pure },
});
runTests("qrcode-6", QRCode, 15, {
{ 15, 15, 0 },
{ 15, 15, 90 },
{ 15, 15, 180 },
{ 15, 15, 270 },
});
runStructuredAppendTest("qrcode-7", QRCode, 1, {
{ 1, 1, 0 },
});
runTests("microqrcode-1", MicroQRCode, 16, {
{ 15, 15, 0 },
{ 14, 14, 90 },
{ 14, 14, 180 }, // ughs: 1 result is platform/compiler dependent (e.g. -march=core2 vs. haswell)
{ 15, 15, 270 },
{ 9, 0, pure },
});
runTests("rmqrcode-1", RMQRCode, 3, {
{ 2, 3, 0 },
{ 2, 3, 90 },
{ 2, 3, 180 },
{ 2, 3, 270 },
{ 2, 2, pure },
});
runTests("pdf417-1", PDF417, 17, {
{ 16, 17, 0 },
{ 1, 17, 90 },
{ 16, 17, 180 },
{ 1, 17, 270 },
{ 16, 0, pure },
});
runTests("pdf417-2", PDF417, 25, {
{ 25, 25, 0 },
{ 0, 25, 90 },
{ 25, 25, 180 },
{ 0, 25, 270 },
});
runTests("pdf417-3", PDF417, 16, {
{ 16, 16, 0 },
{ 0, 16, 90 },
{ 16, 16, 180 },
{ 0, 16, 270 },
{ 7, 0, pure },
});
runStructuredAppendTest("pdf417-4", PDF417, 3, {
{ 3, 3, 0 },
});
runTests("micropdf417-1", MicroPDF417, 5, {
{ 4, 4, 0 },
{ 0, 4, 90 },
{ 4, 4, 180 },
{ 0, 4, 270 },
{ 1, 0, pure },
});
runTests("falsepositives-1", None, 21, {
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 90 },
{ 0, 0, 0, 0, 180 },
{ 0, 0, 0, 0, 270 },
{ 0, 0, pure },
});
runTests("falsepositives-2", None, 25, {
{ 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 90 },
{ 0, 0, 0, 0, 180 },
{ 0, 0, 0, 0, 270 },
{ 0, 0, pure },
});
// clang-format on
int totalTime = timeSince(startTime);
int decodeTime = totalTime - totalImageLoadTime;
std::println("load time: {} ms.", totalImageLoadTime);
std::println("decode time: {} ms.", decodeTime);
std::println("total time: {} ms.", totalTime);
if (failed)
std::println("WARNING: {} tests failed.", failed);
if (extra)
std::println("INFO: {} tests succeeded unexpectedly.", extra);
return failed;
}
catch (const std::exception& e) {
std::println("{}", e.what());
}
catch (...) {
std::println("Internal error");
}
return -1;
}
} // ZXing::Test

18
tools/chatgui/zxing-cpp/test/blackbox/BlackboxTestRunner.h

@ -0,0 +1,18 @@
/*
* Copyright 2016 Nu-book Inc.
*/
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <filesystem>
#include <set>
#include <string>
namespace fs = std::filesystem;
namespace ZXing::Test {
int runBlackBoxTests(const fs::path& testPathPrefix, const std::set<std::string>& includedTests);
} // ZXing::Test

37
tools/chatgui/zxing-cpp/test/blackbox/CMakeLists.txt

@ -0,0 +1,37 @@
zxing_add_package_stb()
zxing_add_package(WebP WebP https://github.com/webmproject/libwebp.git v1.6.0)
if (ZXING_READERS)
add_executable (ReaderTest
TestReaderMain.cpp
ImageLoader.h
ImageLoader.cpp
BlackboxTestRunner.h
BlackboxTestRunner.cpp
)
target_link_libraries(ReaderTest
ZXing::ZXing stb::stb WebP::webp
$<$<AND:$<CXX_COMPILER_ID:GNU>,$<VERSION_LESS:$<CXX_COMPILER_VERSION>,9.0>>:stdc++fs>
)
if (WIN32)
add_custom_command(TARGET ReaderTest POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:ReaderTest>
$<TARGET_FILE_DIR:ReaderTest>
COMMAND_EXPAND_LISTS
VERBATIM
)
endif()
add_test(NAME ReaderTest COMMAND ReaderTest ${CMAKE_CURRENT_SOURCE_DIR}/../samples)
endif()
if (ZXING_WRITERS)
add_executable (WriterTest TestWriterMain.cpp)
target_link_libraries (WriterTest ZXing::ZXing stb::stb)
add_test(NAME WriterTest COMMAND WriterTest)
endif()

105
tools/chatgui/zxing-cpp/test/blackbox/ImageLoader.cpp

@ -0,0 +1,105 @@
/*
* Copyright 2016 Nu-book Inc.
* Copyright 2019 Axel Waggersauser.
*/
// SPDX-License-Identifier: Apache-2.0
#include "ImageLoader.h"
#include "BinaryBitmap.h"
#include "ImageView.h"
#include <array>
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <iterator>
#include <map>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <vector>
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#include <webp/decode.h>
namespace ZXing::Test {
class STBImage : public ImageView
{
std::unique_ptr<uint8_t[], void (*)(void*)> _memory = {nullptr, nullptr};
public:
void load(const fs::path& imgPath)
{
int width = 0, height = 0, channels = 0;
int rowStride = 0;
if (imgPath.extension() == ".webp") {
std::ifstream file(imgPath, std::ios::binary);
if (!file)
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (failed to open file)");
std::vector<uint8_t> compressedData(std::istreambuf_iterator<char>(file), {});
if (compressedData.empty())
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (empty file)");
#if 0
// directly decoding to YUV is faster and makes more sense but a couple of tests fail with this, so we decode to RGB and convert to Lum instead
channels = 1;
uint8_t *u = nullptr, *v = nullptr;
int uvStride = 0;
_memory = std::unique_ptr<uint8_t[], void (*)(void*)>(
WebPDecodeYUV(compressedData.data(), compressedData.size(), &width, &height, &u, &v, &rowStride, &uvStride), WebPFree);
#else
channels = 3;
_memory = std::unique_ptr<uint8_t[], void (*)(void*)>(
WebPDecodeRGB(compressedData.data(), compressedData.size(), &width, &height), WebPFree);
#endif
if (_memory == nullptr)
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (WebP decode failed)");
} else {
_memory = std::unique_ptr<uint8_t[], void (*)(void*)>(stbi_load(imgPath.string().c_str(), &width, &height, &channels, 0), stbi_image_free);
if (_memory == nullptr)
throw std::runtime_error("Failed to read image: " + imgPath.string() + " (" + stbi_failure_reason() + ")");
rowStride = width * channels;
}
auto ImageFormatFromChannels = std::array{ImageFormat::None, ImageFormat::Lum, ImageFormat::LumA, ImageFormat::RGB, ImageFormat::RGBA};
ImageView::operator=({_memory.get(), width, height, ImageFormatFromChannels.at(channels), rowStride});
// preconvert from RGB -> Lum to do this only once instead of for each rotation
if (_format == ImageFormat::RGB) {
auto* img = _memory.get();
for (int i = 0; i < width * height; ++i)
img[i] = RGBToLum(img[3 * i + 0], img[3 * i + 1], img[3 * i + 2]);
ImageView::operator=({_memory.get(), width, height, ImageFormat::Lum});
}
}
operator bool() const { return _data; }
};
std::map<fs::path, STBImage> cache;
std::mutex cacheMutex;
void ImageLoader::clearCache()
{
std::scoped_lock lock(cacheMutex);
cache.clear();
}
const ImageView& ImageLoader::load(const fs::path& imgPath)
{
std::scoped_lock lock(cacheMutex);
auto& binImg = cache[imgPath];
if (!binImg)
binImg.load(imgPath);
return binImg;
}
} // namespace ZXing::Test

18
tools/chatgui/zxing-cpp/test/blackbox/ImageLoader.h

@ -0,0 +1,18 @@
/*
* Copyright 2016 Nu-book Inc.
*/
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "ImageView.h"
#include <filesystem>
namespace fs = std::filesystem;
namespace ZXing::Test::ImageLoader {
void clearCache();
const ImageView& load(const fs::path& imgPath);
} // namespace ZXing::Test::ImageLoader

65
tools/chatgui/zxing-cpp/test/blackbox/TestReaderMain.cpp

@ -0,0 +1,65 @@
/*
* Copyright 2016 Nu-book Inc.
* Copyright 2017 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "BlackboxTestRunner.h"
#include "ImageLoader.h"
#include "ReadBarcode.h"
#include "StdPrint.h"
#include "ZXAlgorithms.h"
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <set>
using namespace ZXing;
using namespace ZXing::Test;
int getEnv(const char* name, int fallback = 0)
{
auto var = getenv(name);
return var ? std::stoi(var) : fallback;
}
int main(int argc, char** argv)
{
if (argc <= 1) {
std::println("Usage: {} <test_path_prefix>", argv[0]);
return 0;
}
fs::path pathPrefix = argv[1];
if (Contains({".png", ".jpg", ".pgm", ".gif", ".webp", ".jxl"}, pathPrefix.extension())) {
auto opts = ReaderOptions().tryHarder(!getEnv("FAST", false)).tryRotate(true).isPure(getEnv("IS_PURE"));
if (getenv("FORMATS"))
opts.formats(BarcodeFormatsFromString(getenv("FORMATS")));
int rotation = getEnv("ROTATION");
for (int i = 1; i < argc; ++i) {
Barcode barcode = ReadBarcode(ImageLoader::load(argv[i]).rotated(rotation), opts);
std::print("{}: ", argv[i]);
if (barcode.isValid())
std::println("{}: {}", ToString(barcode.format()), barcode.text());
else
std::println("FAILED");
if (barcode.isValid() && getenv("WRITE_TEXT")) {
std::ofstream f(fs::path(argv[i]).replace_extension(".txt"));
f << barcode.text();
}
}
return 0;
} else {
std::set<std::string> includedTests;
for (int i = 2; i < argc; ++i) {
if (std::strlen(argv[i]) > 2 && argv[i][0] == '-' && argv[i][1] == 't') {
includedTests.insert(argv[i] + 2);
}
}
return runBlackBoxTests(pathPrefix, includedTests);
}
}

48
tools/chatgui/zxing-cpp/test/blackbox/TestWriterMain.cpp

@ -0,0 +1,48 @@
/*
* Copyright 2016 Nu-book Inc.
* Copyright 2019 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "ZXingCpp.h"
#include <vector>
using namespace ZXing;
using namespace std::literals;
using enum BarcodeFormat;
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h>
void savePng(ImageView iv, BarcodeFormat format)
{
stbi_write_png((ToString(format) + ".png"s).c_str(), iv.width(), iv.height(), iv.pixStride(), iv.data(), iv.rowStride());
}
int main()
{
std::string text = "zxing-cpp";
for (auto format : BarcodeFormats::list(AllMatrix))
if (format & AllCreatable && format != AztecRune)
savePng(CreateBarcodeFromText(text, format).symbol(), format);
text = "012345678901234567890123456789";
using FormatSpecs = std::vector<std::pair<BarcodeFormat, size_t>>;
for (const auto& [format, length] : FormatSpecs({
// {Codabar, 0}, // needs to start with A, B, C or D
{Code39, 0},
{Code93, 0},
{Code128, 0},
{EAN8, 7},
{EAN13, 12},
{ITF, 0},
{UPCA, 11},
{UPCE, 7}
}))
{
auto input = length > 0 ? text.substr(0, length) : text;
if (format & AllCreatable)
savePng(WriteBarcodeToImage(CreateBarcodeFromText(input, format)), format);
}
}

34
tools/chatgui/zxing-cpp/test/fuzz/CMakeLists.txt

@ -0,0 +1,34 @@
cmake_minimum_required (VERSION 3.16)
# -fsanitize=fuzzer only available in clang / llvm
# cmake -DCMAKE_CXX_COMPILER=/path/to/your/clang++
project (ZXingFuzz)
set (CMAKE_CXX_STANDARD 20)
set (CMAKE_CXX_STANDARD_REQUIRED ON)
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -march=native -fsanitize=address,undefined,fuzzer")
set (ZXING_WRITERS BOTH)
set (ZXING_READERS ON)
add_definitions (-DZXING_BUILD_FOR_TEST)
add_subdirectory (${CMAKE_CURRENT_SOURCE_DIR}/../../core ${CMAKE_BINARY_DIR}/ZXing)
set (TESTS
DBEDecoder
DMEncoder
ReadLinear
ReadMatrix
DecodeMatrix
)
foreach (test ${TESTS})
set (name "fuzz${test}")
add_executable (${name} "${name}.cpp")
target_compile_options (${name} PRIVATE -DZXING_INTERNAL)
target_link_libraries (${name} ZXing::ZXing)
endforeach()

31
tools/chatgui/zxing-cpp/test/fuzz/fuzzDBEDecoder.cpp

@ -0,0 +1,31 @@
/*
* Copyright 2021 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include <stdint.h>
#include <stddef.h>
#include "BitArray.h"
#include "Error.h"
#include "oned/ODDataBarExpandedBitDecoder.h"
using namespace ZXing;
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
if (size < 2)
return 0;
BitArray bits;
for (size_t i = 0; i < size; ++i)
bits.appendBits(data[i], 8);
try {
OneD::DataBar::DecodeExpandedBits(bits);
} catch (std::out_of_range) {
} catch (Error) {
}
return 0;
}

24
tools/chatgui/zxing-cpp/test/fuzz/fuzzDMEncoder.cpp

@ -0,0 +1,24 @@
/*
* Copyright 2021 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include <stdint.h>
#include <stddef.h>
#include "ByteArray.h"
#include "datamatrix/DMHighLevelEncoder.h"
using namespace ZXing;
using namespace ZXing::DataMatrix;
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
std::wstring txt(data, data + size);
try {
Encode(txt);
} catch (...) {
}
return 0;
}

87
tools/chatgui/zxing-cpp/test/fuzz/fuzzDecodeMatrix.cpp

@ -0,0 +1,87 @@
/*
* Copyright 2023 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "BitArray.h"
#include "ByteArray.h"
#include "DecoderResult.h"
#include "qrcode/QRErrorCorrectionLevel.h"
#include "qrcode/QRVersion.h"
#include "pdf417/PDFDecoder.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
using namespace ZXing;
namespace ZXing::Aztec {
DecoderResult Decode(const BitArray& bits);
}
namespace ZXing::DataMatrix::DecodedBitStreamParser {
DecoderResult Decode(ByteArray&& bytes, const bool isDMRE);
}
namespace ZXing::QRCode {
DecoderResult DecodeBitStream(ByteArray&& bytes, const Version& version, ErrorCorrectionLevel ecLevel);
}
void az(const uint8_t* data, size_t size)
{
BitArray bits;
for (size_t i = 1; i < size - 1; ++i)
bits.appendBits(data[i], 8);
bits.appendBits(data[size - 1], (data[0] & 0x7) + 1);
Aztec::Decode(bits);
}
void dm(const uint8_t* data, size_t size)
{
ByteArray ba;
ba.insert(ba.begin(), data, data + size);
try {
DataMatrix::DecodedBitStreamParser::Decode(std::move(ba), false);
} catch (...) {
}
}
void qr(const uint8_t* data, size_t size)
{
auto version = QRCode::Version::Model2(std::clamp(data[0] & 0x3F, 1, 40));
auto ecLevel = QRCode::ECLevelFromBits(data[0] >> 6);
ByteArray ba;
ba.insert(ba.begin(), data, data + size);
QRCode::DecodeBitStream(std::move(ba), *version, ecLevel);
}
void pd(const uint8_t* data, size_t size)
{
auto codewords = std::vector<int>(size / 2);
auto u16 = reinterpret_cast<const uint16_t*>(data);
for (int i = 0; i < static_cast<int>(codewords.size()); ++i)
codewords[i] = u16[i] % 929;
codewords[0] = static_cast<int>(codewords.size());
Pdf417::Decode(codewords);
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
if (size < 3)
return 0;
az(data, size);
dm(data, size);
qr(data, size);
pd(data, size);
return 0;
}

68
tools/chatgui/zxing-cpp/test/fuzz/fuzzReadLinear.cpp

@ -0,0 +1,68 @@
/*
* Copyright 2021 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include <stdint.h>
#include <stddef.h>
#include "oned/ODMultiUPCEANReader.h"
#include "oned/ODCode39Reader.h"
#include "oned/ODCode93Reader.h"
#include "oned/ODCode128Reader.h"
#include "oned/ODDataBarReader.h"
#include "oned/ODDataBarExpandedReader.h"
#include "oned/ODDXFilmEdgeReader.h"
#include "oned/ODITFReader.h"
#include "oned/ODCodabarReader.h"
#include "ReaderOptions.h"
#include "Barcode.h"
using namespace ZXing;
using namespace ZXing::OneD;
static std::vector<std::unique_ptr<RowReader>> readers;
bool init()
{
static auto opts = ReaderOptions().returnErrors(true);
readers.emplace_back(new MultiUPCEANReader(opts));
readers.emplace_back(new Code39Reader(opts));
readers.emplace_back(new Code93Reader(opts));
readers.emplace_back(new Code128Reader(opts));
readers.emplace_back(new ITFReader(opts));
readers.emplace_back(new CodabarReader(opts));
readers.emplace_back(new DataBarReader(opts));
readers.emplace_back(new DataBarExpandedReader(opts));
readers.emplace_back(new DXFilmEdgeReader(opts));
return true;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
if (size < 1)
return 0;
static bool inited [[maybe_unused]] = init();
std::vector<std::unique_ptr<RowReader::DecodingState>> decodingState(readers.size());
PatternRow row(size * 2 + 1);
for (size_t i = 0; i < size; ++i){
auto v = data[i];
row[i * 2 + 0] = (v & 0xf) + 1;
row[i * 2 + 1] = (v >> 4) + 1;
}
row.back() = 0;
for (size_t r = 0; r < readers.size(); ++r) {
PatternView next(row);
while (next.isValid()) {
readers[r]->decodePattern(0, next, decodingState[r]);
// make sure we make progress and we start the next try on a bar
next.shift(2 - (next.index() % 2));
next.extend();
}
}
return 0;
}

64
tools/chatgui/zxing-cpp/test/fuzz/fuzzReadMatrix.cpp

@ -0,0 +1,64 @@
/*
* Copyright 2023 Axel Waggershauser
*/
// SPDX-License-Identifier: Apache-2.0
#include "ReadBarcode.h"
#include <algorithm>
#include <cassert>
#include <csignal>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
using namespace ZXing;
uint64_t Expand(uint8_t b)
{
uint64_t shift = 0x0000040810204081ul; // bits set: 0, 7, 14, 21, 28, 35, 42
uint64_t mask = 0x0001010101010101ul; // bits set: 0, 8, 16, 24, 32, 40, 48
return ((uint64_t)(b & 127) * shift & mask) | (uint64_t)(b & 128) << 49;
}
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
if (size < 3)
return 0;
static auto opts = ReaderOptions()
.formats(BarcodeFormat::AllMatrix)
.binarizer(Binarizer::BoolCast)
.returnErrors(true)
.tryInvert(false)
.tryRotate(false);
int ratio = data[0] + 1;
int nBits = (size - 1) * 8;
int width = std::clamp(nBits * ratio / 256, 1, nBits);
int height = std::clamp(nBits / width, 1, nBits);
assert(width * height <= nBits);
std::vector<uint8_t> buffer(nBits);
for (size_t i = 1; i < size; ++i)
*reinterpret_cast<uint64_t*>(&buffer[(i - 1) * 8]) = Expand(data[i]);
#ifdef PRINT_DEBUG
printf("s: %zu, r: %d, n: %d -> %d x %d\n", size, ratio, nBits, width, height);
#endif
auto image = ImageView(buffer.data(), width, height, ImageFormat::Lum);
auto res = ReadBarcodes(image, opts);
#ifdef PRINT_DEBUG
for (const auto& r : res)
printf("%s: %s / %s\n", ToString(r.format()).c_str(), r.text().c_str(), ToString(r.error()).c_str());
#endif
static int detectedSybols = 0;
detectedSybols += static_cast<int>(res.size());
if (!res.empty() && detectedSybols % 100 == 0)
printf("detected barcode symbols: %d\n", detectedSybols);
return 0;
}

17
tools/chatgui/zxing-cpp/test/install/CMakeLists.txt

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.16)
project(ZXingInstallTest LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) # make sure the public API is still c++17 compatible
find_package(ZXing CONFIG REQUIRED)
add_executable(InstallTest main.cpp)
target_link_libraries(InstallTest PRIVATE ZXing::ZXing)
if (ZXING_WRITERS MATCHES "BOTH|OLD")
add_executable(InstallTestOLD mainOLD.cpp)
target_link_libraries(InstallTestOLD PRIVATE ZXing::ZXing)
endif()

22
tools/chatgui/zxing-cpp/test/install/main.cpp

@ -0,0 +1,22 @@
#include "ZXing/ZXingCpp.h"
#include <iostream>
int main(int argc, char** argv)
{
using namespace ZXing;
auto copts = CreatorOptions(BarcodeFormat::QRCode, "eclevel=L");
auto barcode = CreateBarcodeFromText("Test", copts);
auto wopts = WriterOptions().scale(2);
auto img = WriteBarcodeToImage(barcode, wopts);
auto iv = ImageView(img.data(), img.width(), img.height(), img.format());
auto ropts = ReaderOptions().formats(BarcodeFormat::AllLinear);
auto barcodes = ReadBarcodes(iv, ropts);
for (const auto& b : barcodes)
std::cout << ToString(b.format()) << ": " << b.text() << "\n";
return 0;
}

20
tools/chatgui/zxing-cpp/test/install/mainOLD.cpp

@ -0,0 +1,20 @@
#include "ZXing/MultiFormatWriter.h"
#include "ZXing/BitMatrix.h"
#include "ZXing/BitMatrixIO.h"
#include "ZXing/ZXVersion.h"
#include <iostream>
int main(int argc, char** argv)
{
using namespace ZXing;
std::cout << "zxing-cpp version " << ZXING_VERSION_STR << "\n";
MultiFormatWriter writer(BarcodeFormat::QRCode);
BitMatrix bitmatrix = writer.encode("Hello, World!", 200, 200);
auto matrix = ToMatrix<int8_t>(bitmatrix);
std::cout << ToString(bitmatrix) << "\n";
return 0;
}

34
tools/chatgui/zxing-cpp/test/install/run.cmake

@ -0,0 +1,34 @@
set(install_prefix "${PROJECT_BINARY_DIR}/InstallTest-prefix")
set(consumer_build "${PROJECT_BINARY_DIR}/InstallTest-build")
# 1. Install zxing-cpp
execute_process(
COMMAND "${CMAKE_COMMAND}" --install "${PROJECT_BINARY_DIR}" --prefix "${install_prefix}"
RESULT_VARIABLE install_result
)
if(NOT install_result EQUAL 0)
message(FATAL_ERROR "Install step failed")
endif()
# 2. Configure consumer project
execute_process(
COMMAND
"${CMAKE_COMMAND}"
-S "${SOURCE_DIR}"
-B "${consumer_build}"
-DCMAKE_PREFIX_PATH=${install_prefix}
-DZXING_WRITERS=${ZXING_WRITERS}
RESULT_VARIABLE configure_result
)
if(NOT configure_result EQUAL 0)
message(FATAL_ERROR "Consumer configure failed")
endif()
# 3. Build consumer project
execute_process(
COMMAND "${CMAKE_COMMAND}" --build "${consumer_build}"
RESULT_VARIABLE build_result
)
if(NOT build_result EQUAL 0)
message(FATAL_ERROR "Consumer build failed")
endif()

1
tools/chatgui/zxing-cpp/test/samples/.gitattributes vendored

@ -0,0 +1 @@
* binary

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/#128.txt

@ -0,0 +1 @@
This is an example Aztec symbol for Wikipedia.

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/#128.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/#217.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/#217.txt

@ -0,0 +1 @@
I have the best words.

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/7.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/7.result.txt

@ -0,0 +1 @@
symbologyIdentifier=]z0

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/7.txt

@ -0,0 +1 @@
Code 2D!

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/BinarizerFallback.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/BinarizerFallback.txt

@ -0,0 +1 @@
This is an example Aztec symbol for Wikipedia.

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/Historico.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/Historico.txt

@ -0,0 +1 @@
Histórico

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/HistoricoLong.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/HistoricoLong.txt

@ -0,0 +1 @@
Históóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóóórico

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/Z1-sequence4of7.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

4
tools/chatgui/zxing-cpp/test/samples/aztec-1/Z1-sequence4of7.result.txt

@ -0,0 +1,4 @@
symbologyIdentifier=]z6
sequenceSize=7
sequenceIndex=3
sequenceId=Z1.txt

1
tools/chatgui/zxing-cpp/test/samples/aztec-1/Z1-sequence4of7.txt

@ -0,0 +1 @@
3456

BIN
tools/chatgui/zxing-cpp/test/samples/aztec-1/abc-19x19C.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save