Validate avatar read recovery and bounded typed blobs

This commit is contained in:
evgeny committed 2026-10-27 11:57:30 +01:00
1 parent 6106fc1b5d
commit bd105c630d
2 files changed
+62 -12

No files matched your search

@@ -48,7 +48,7 @@ QImage GroupAvatarManager::image(const QString& channel, quint64 version, const
bool read = false;
const QByteArray bytes = m_db->getChannelAvatar(channel, &read);
if (!read) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: database snapshot unavailable, cache unchanged channel=%s", qPrintable(channel)); return {};
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: database snapshot unavailable, image not cached channel=%s", qPrintable(channel)); return {};
}
m_loading.insert(key);
m_pool.start(QRunnable::create([this, key, bytes, hash, channel] {
@@ -128,11 +128,14 @@ void GroupAvatarManager::upload(const QString& channel, const QString& source) {
void GroupAvatarManager::remove(const QString& channel) { publish(channel, {}, ++m_generation[channel]); }
void GroupAvatarManager::publish(const QString& channel, const QByteArray& bytes, quint64 generation) {
const quint64 group = channel.toULongLong();
if (!m_db || !m_db->isOpen() || !gui_bridge_is_uasync_ready() || !group || group > quint64(std::numeric_limits<qint64>::max())
|| QString::number(group) != channel || bytes.size() > GROUP_AVATAR_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication unavailable channel=%s", qPrintable(channel));
if (!m_db || !m_db->isOpen() || !gui_bridge_is_uasync_ready()) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication requires running chat and open DB channel=%s", qPrintable(channel));
emit status(channel, text("Chat is stopped")); return;
}
if (!group || group > quint64(std::numeric_limits<qint64>::max()) || QString::number(group) != channel || bytes.size() > GROUP_AVATAR_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: invalid publication channel_bytes=%d image_bytes=%d", int(channel.toUtf8().size()), int(bytes.size()));
emit status(channel, text("Cannot update group avatar")); return;
}
auto* q = (group_avatar_gui_request*)u_calloc(1, sizeof(group_avatar_gui_request));
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group avatar: GUI publication allocation failed"); emit status(channel, text("Cannot update group avatar")); return; }
q->inst = gui_bridge_get_inst(); q->group = group; q->size = bytes.size();
@@ -168,7 +171,7 @@ void GroupAvatarManager::onResult(const uint8_t* bytes, int length) {
if (m_generation.value(request.first) != request.second) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: ignoring stale publication result request=%llu", (unsigned long long)id); return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: publication completed request=%llu channel=%s result=%u", (unsigned long long)id,
qPrintable(request.first), bytes[16]);
if (bytes[16]) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "group avatar: publication failed request=%llu channel=%s", (unsigned long long)id, qPrintable(request.first));
else DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "group avatar: publication completed request=%llu channel=%s", (unsigned long long)id, qPrintable(request.first));
emit status(request.first, bytes[16] ? text("Cannot update group avatar") : text("Group avatar updated"));
}
+53 -6
View File
@@ -288,7 +288,13 @@ private slots:
QCOMPARE(GroupAvatarManager::instance(), &current);
}
void avatarReadFailureCanBeRetried_data() {
QTest::addColumn<bool>("prepare");
QTest::newRow("prepare-denied") << true;
QTest::newRow("step-interrupted") << false;
}
void avatarReadFailureCanBeRetried() {
QFETCH(bool, prepare);
SettingsCore core; QVERIFY(core.start());
QImage image(32, 32, QImage::Format_RGB32); image.fill(Qt::green);
QByteArray jpeg; QBuffer output(&jpeg); QVERIFY(output.open(QIODevice::WriteOnly)); QVERIFY(image.save(&output, "JPEG"));
@@ -296,18 +302,59 @@ private slots:
const auto row = core.db.getChannels()[0];
GroupAvatarManager avatars(&core.db); QSignalSpy changed(&avatars, &GroupAvatarManager::changed);
bool denied = false;
QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db,
[](void* context, int action, const char*, const char* column, const char*, const char*) {
if (action != SQLITE_READ || !column || strcmp(column, "avatar_data")) return SQLITE_OK;
*static_cast<bool*>(context) = true; return SQLITE_DENY;
}, &denied), SQLITE_OK);
auto restore = qScopeGuard([&] { sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr); });
if (prepare) QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db,
[](void* context, int action, const char*, const char* column, const char*, const char*) {
if (action != SQLITE_READ || !column || strcmp(column, "avatar_data")) return SQLITE_OK;
*static_cast<bool*>(context) = true; return SQLITE_DENY;
}, &denied), SQLITE_OK);
else sqlite3_progress_handler(core.inst->topo_sqlite_db, 1, [](void* context) { *static_cast<bool*>(context) = true; return 1; }, &denied);
auto restore = qScopeGuard([&] {
sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr); sqlite3_progress_handler(core.inst->topo_sqlite_db, 0, nullptr, nullptr);
});
QVERIFY(avatars.image(core.channel, row.avatarVersion, row.avatarHash).isNull()); QVERIFY(denied);
QCOMPARE(sqlite3_set_authorizer(core.inst->topo_sqlite_db, nullptr, nullptr), SQLITE_OK);
sqlite3_progress_handler(core.inst->topo_sqlite_db, 0, nullptr, nullptr);
avatars.image(core.channel, row.avatarVersion, row.avatarHash);
QTRY_VERIFY_WITH_TIMEOUT(!avatars.currentImage(core.channel).isNull(), 1000);
QCOMPARE(changed.count(), 1);
}
void avatarBlobTypesAndLimits_data() {
QTest::addColumn<QByteArray>("value"); QTest::addColumn<bool>("valid"); QTest::addColumn<int>("bytes");
QTest::newRow("empty-blob") << QByteArray("zeroblob(0)") << true << 0;
QTest::newRow("maximum-blob") << QByteArray("zeroblob(16384)") << true << 16384;
QTest::newRow("oversized-blob") << QByteArray("zeroblob(16385)") << false << 0;
QTest::newRow("null") << QByteArray("NULL") << false << 0;
QTest::newRow("text") << QByteArray("'jpeg'") << false << 0;
QTest::newRow("integer") << QByteArray("1") << false << 0;
}
void avatarBlobTypesAndLimits() {
QFETCH(QByteArray, value); QFETCH(bool, valid); QFETCH(int, bytes);
SettingsCore core; QVERIFY(core.start());
const QByteArray sql = "UPDATE channels SET avatar_data=" + value;
QCOMPARE(sqlite3_exec(core.inst->topo_sqlite_db, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK);
bool ok = !valid; const QByteArray result = core.db.getChannelAvatar(core.channel, &ok);
QCOMPARE(ok, valid); QCOMPARE(result.size(), bytes);
QVERIFY(core.db.getChannelAvatar("missing", &ok).isEmpty()); QVERIFY(!ok);
}
#ifdef UTUN_TEST_SETTINGS_ALLOC_FAILURES
void avatarAllocationFailure_data() {
QTest::addColumn<QByteArray>("source");
QTest::newRow("request") << QByteArray("groupavatarmanager.cpp");
QTest::newRow("bridge") << QByteArray("gui_bridge_impl.cpp");
}
void avatarAllocationFailure() {
QFETCH(QByteArray, source);
SettingsCore core; QVERIFY(core.start()); GroupAvatarManager avatars(&core.db);
QSignalSpy statuses(&avatars, &GroupAvatarManager::status); const auto baseline = u_get_allocated_count();
allocationFile = source.constData(); failAllocation = 1;
auto reset = qScopeGuard([] { failAllocation = 0; allocationFile = nullptr; });
avatars.remove(core.channel); QCOMPARE(failAllocation, 0);
QCOMPARE(statuses.count(), 1); QCOMPARE(statuses[0][1].toString(), QString("Cannot update group avatar"));
gui_bridge_set_inst(nullptr); QCOMPARE(u_get_allocated_count(), baseline);
}
#endif
};
QTEST_MAIN(ChannelSettingsTest)