Reject incomplete DB reads and clock voice progress from PCM outside paint

This commit is contained in:
evgeny committed 2026-10-28 00:59:00 +01:00
1 parent 1235f770eb
commit 034df2939c
2 files changed
+84 -119

No files matched your search

+66 -77
View File
@@ -97,7 +97,8 @@ QList<ChannelRow> DbManager::getChannels(ChannelSortMode sort) const {
" FROM channels%s", orderBy);
sqlite3_stmt* stmt = prepareOrNull(sql);
if (!stmt) return list;
while (sqlite3_step(stmt) == SQLITE_ROW) {
int rc;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
ChannelRow r;
r.channelId = colText(stmt, 0);
r.name = colText(stmt, 1);
@@ -115,7 +116,7 @@ QList<ChannelRow> DbManager::getChannels(ChannelSortMode sort) const {
r.avatarHash = colBlob(stmt, 13);
list.append(r);
}
sqlite3_finalize(stmt);
if (!finishRead(stmt, rc)) return {};
for (auto& ch : list) {
ch.lastReadMsgId = getUiStateInt64(
QStringLiteral("ch_%1_last_read").arg(ch.channelId), 0);
@@ -165,11 +166,9 @@ bool DbManager::isChannelMember(const QString& chId, quint64 nodeId) const {
const QByteArray sql = QStringLiteral("SELECT 1 FROM \"%1\" WHERE node_id=?").arg(peersTableName(chId)).toUtf8();
sqlite3_stmt* statement = prepareOrNull(sql.constData());
if (!statement) return false;
sqlite3_bind_int64(statement, 1, (sqlite3_int64)nodeId);
const int result = sqlite3_step(statement);
if (result != SQLITE_ROW && result != SQLITE_DONE) GUI_ERROR("channel membership read failed ch=%s rc=%d", qPrintable(chId), result);
sqlite3_finalize(statement);
return result == SQLITE_ROW;
const int bound = sqlite3_bind_int64(statement, 1, (sqlite3_int64)nodeId);
const int result = bound == SQLITE_OK ? sqlite3_step(statement) : bound;
return finishRead(statement, result) && result == SQLITE_ROW;
}
/* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */
@@ -227,7 +226,8 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const {
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return {};
QString result;
if (sqlite3_step(stmt) == SQLITE_ROW) {
const int rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
quint64 author = (quint64)sqlite3_column_int64(stmt, 0);
QByteArray jdata = colBlob(stmt, 1);
QString ct = QString::fromUtf8(parseJsonField(jdata, "ct"));
@@ -235,8 +235,7 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const {
QString nick = getDisplayName(author);
result = nick + QStringLiteral(": ") + mediaPreviewBody(ct, d);
}
sqlite3_finalize(stmt);
return result;
return finishRead(stmt, rc) ? result : QString();
}
/* ── messages (UI) ── */
@@ -265,7 +264,7 @@ static MessageRow readMessage(sqlite3_stmt* stmt, quint64 myId) {
/* Окно вокруг оригинала выбирается одним SQL snapshot, включая одинаковые timestamp. */
QList<MessageRow> DbManager::getMessages(const QString& chId, int limit, const QByteArray& around) const {
QList<MessageRow> list;
if (!around.isEmpty() && (around.size() != 32 || limit <= 0)) {
if (limit <= 0 || (!around.isEmpty() && around.size() != 32)) {
GUI_ERROR("history: invalid anchor/window bytes=%d limit=%d", int(around.size()), limit); return list;
}
QString sql = QStringLiteral(
@@ -277,19 +276,24 @@ QList<MessageRow> DbManager::getMessages(const QString& chId, int limit, const Q
"max(0,(SELECT count(*) FROM \"%1\")-?1))").arg(msgTableName(chId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return list;
sqlite3_bind_int(stmt, 1, limit);
if (!around.isEmpty()) sqlite3_bind_blob(stmt, 2, around.constData(), around.size(), SQLITE_TRANSIENT);
uint64_t myId = 0;
{ sqlite3_stmt* ms = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1");
if (ms) { if (sqlite3_step(ms) == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(ms, 0); sqlite3_finalize(ms); } }
int rc;
int rc = sqlite3_bind_int(stmt, 1, limit);
if (rc == SQLITE_OK && !around.isEmpty()) rc = sqlite3_bind_blob(stmt, 2, around.constData(), around.size(), SQLITE_TRANSIENT);
if (rc != SQLITE_OK) { finishRead(stmt, rc); return {}; }
uint64_t myId = m_myNodeId;
if (!myId) {
sqlite3_stmt* identity = prepareOrNull("SELECT node_id FROM local_identity WHERE id=1");
if (identity) {
const int row = sqlite3_step(identity);
if (row == SQLITE_ROW) myId = (uint64_t)sqlite3_column_int64(identity, 0);
if (!finishRead(identity, row)) { finishRead(stmt, SQLITE_ABORT); return {}; }
}
}
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
list.prepend(readMessage(stmt, myId));
}
if (rc != SQLITE_DONE) GUI_ERROR("history: read failed ch=%s rc=%d", qPrintable(chId), rc);
if (!finishRead(stmt, rc)) return {};
if (!around.isEmpty()) GUI_DEBUG("history: window loaded ch=%s anchor=%s rows=%d limit=%d",
qPrintable(chId), around.toHex().constData(), int(list.size()), limit);
sqlite3_finalize(stmt);
return list;
}
@@ -300,13 +304,13 @@ QList<MessageRow> DbManager::getMessagesSince(const QString& chId, qint64 sinceI
" FROM \"%1\" WHERE id > ? ORDER BY id ASC").arg(msgTableName(chId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return list;
sqlite3_bind_int64(stmt, 1, sinceId);
int rc = sqlite3_bind_int64(stmt, 1, sinceId);
if (rc != SQLITE_OK) { finishRead(stmt, rc); return {}; }
uint64_t myId = m_myNodeId;
while (sqlite3_step(stmt) == SQLITE_ROW) {
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
list.append(readMessage(stmt, myId));
}
sqlite3_finalize(stmt);
return list;
return finishRead(stmt, rc) ? list : QList<MessageRow>();
}
MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const {
@@ -316,12 +320,12 @@ MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const {
" FROM \"%1\" WHERE id = ?").arg(msgTableName(chId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return m;
sqlite3_bind_int64(stmt, 1, msgId);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int rc = sqlite3_bind_int64(stmt, 1, msgId);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
m = readMessage(stmt, m_myNodeId);
}
sqlite3_finalize(stmt);
return m;
return finishRead(stmt, rc) ? m : MessageRow();
}
/* ── DM-беседы (UI, read-only) ── */
@@ -334,12 +338,10 @@ MessageRow DbManager::getMessageByKey(const QString& chId, const QByteArray& key
"FROM \"%1\" WHERE message_id=?").arg(msgTableName(chId)).toUtf8();
sqlite3_stmt* stmt = prepareOrNull(sql.constData());
if (!stmt) return message;
sqlite3_bind_blob(stmt, 1, key.constData(), key.size(), SQLITE_TRANSIENT);
const int rc = sqlite3_step(stmt);
int rc = sqlite3_bind_blob(stmt, 1, key.constData(), key.size(), SQLITE_TRANSIENT);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) message = readMessage(stmt, m_myNodeId);
else if (rc != SQLITE_DONE) GUI_ERROR("reply: lookup failed ch=%s rc=%d", qPrintable(chId), rc);
sqlite3_finalize(stmt);
return message;
return finishRead(stmt, rc) ? message : MessageRow();
}
/* Короткий обычный текст без рекурсивных цитат и служебного описания медиа. */
@@ -378,12 +380,7 @@ QList<DmConversation> DbManager::getDmConversations() const {
c.generation = (quint64)sqlite3_column_int64(stmt, 6);
list.append(c);
}
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM list: read failed rc=%d sql=%s", rc, sqlite3_errmsg(m_db));
list.clear();
}
sqlite3_finalize(stmt);
return list;
return finishRead(stmt, rc) ? list : QList<DmConversation>();
}
/* ── peers (UI) ── */
@@ -396,31 +393,21 @@ QMap<quint64, QString> DbManager::getLocalNicknames(const QString& chId) const {
).arg(peersTableName(chId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return m;
while (sqlite3_step(stmt) == SQLITE_ROW)
int rc;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
m.insert((quint64)sqlite3_column_int64(stmt, 0), colText(stmt, 1));
sqlite3_finalize(stmt);
return m;
return finishRead(stmt, rc) ? m : QMap<quint64, QString>();
}
QString DbManager::getDisplayName(quint64 nodeId) const {
sqlite3_stmt* st = prepareOrNull(
"SELECT display_name FROM accounts WHERE node_id=?");
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
if (sqlite3_step(st) == SQLITE_ROW) {
QString s = colText(st, 0);
sqlite3_finalize(st);
if (!s.isEmpty()) return s;
} else sqlite3_finalize(st);
}
st = prepareOrNull("SELECT name FROM nodes WHERE node_id=?");
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
if (sqlite3_step(st) == SQLITE_ROW) {
QString s = colText(st, 0);
sqlite3_finalize(st);
if (!s.isEmpty()) return s;
} else sqlite3_finalize(st);
for (const char* sql : {"SELECT display_name FROM accounts WHERE node_id=?", "SELECT name FROM nodes WHERE node_id=?"}) {
sqlite3_stmt* st = prepareOrNull(sql);
if (!st) continue;
int rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
QString name = rc == SQLITE_ROW ? colText(st, 0) : QString();
if (!finishRead(st, rc)) break;
if (!name.isEmpty()) return name;
}
return QString("node_%1").arg(nodeId);
}
@@ -428,10 +415,12 @@ QString DbManager::getDisplayName(quint64 nodeId) const {
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, (sqlite3_int64)nodeId);
int rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
QByteArray r;
if (sqlite3_step(stmt) == SQLITE_ROW) r = colBlob(stmt, 0);
sqlite3_finalize(stmt);
if (rc == SQLITE_ROW) r = colBlob(stmt, 0);
if (!finishRead(stmt, rc)) return {};
if (!r.isEmpty() && r.size() != 32) { GUI_ERROR("node signing key has invalid length=%d", int(r.size())); return {}; }
return r;
}
@@ -446,7 +435,8 @@ QList<AccountRow> DbManager::getAccounts(bool contactsOnly) const {
" FROM accounts ORDER BY display_name";
sqlite3_stmt* stmt = prepareOrNull(sql);
if (!stmt) return list;
while (sqlite3_step(stmt) == SQLITE_ROW) {
int rc;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
AccountRow a;
a.nodeId = (quint64)sqlite3_column_int64(stmt, 0);
a.displayName = colText(stmt, 1);
@@ -455,8 +445,7 @@ QList<AccountRow> DbManager::getAccounts(bool contactsOnly) const {
a.isContact = sqlite3_column_int(stmt, 4) != 0;
list.append(a);
}
sqlite3_finalize(stmt);
return list;
return finishRead(stmt, rc) ? list : QList<AccountRow>();
}
AccountRow DbManager::getAccount(quint64 nodeId) const {
@@ -465,16 +454,16 @@ AccountRow DbManager::getAccount(quint64 nodeId) const {
"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, (sqlite3_int64)nodeId);
if (sqlite3_step(stmt) == SQLITE_ROW) {
int rc = sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
if (rc == SQLITE_ROW) {
a.nodeId = (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;
return finishRead(stmt, rc) ? a : AccountRow();
}
/* ── ui_state ── */
@@ -482,11 +471,11 @@ AccountRow DbManager::getAccount(quint64 nodeId) const {
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_TRANSIENT);
int rc = sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
QString val;
if (sqlite3_step(stmt) == SQLITE_ROW) val = colText(stmt, 0);
sqlite3_finalize(stmt);
return val;
if (rc == SQLITE_ROW) val = colText(stmt, 0);
return finishRead(stmt, rc) ? val : QString();
}
int DbManager::getUiStateInt(const QString& key, int defaultVal) const {
@@ -510,11 +499,11 @@ int DbManager::getUnreadCount(const QString& channelId, qint64 lastReadMsgId) co
"SELECT COUNT(*) FROM \"%1\" WHERE id > ?").arg(msgTableName(channelId));
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return 0;
sqlite3_bind_int64(stmt, 1, lastReadMsgId);
int rc = sqlite3_bind_int64(stmt, 1, lastReadMsgId);
if (rc == SQLITE_OK) rc = sqlite3_step(stmt);
int count = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return count;
if (rc == SQLITE_ROW) count = sqlite3_column_int(stmt, 0);
return finishRead(stmt, rc) ? count : 0;
}
/* ── integrity checks ── */
+18 -42
View File
@@ -26,6 +26,7 @@
#include <QDebug>
#include <QImageReader>
#include <QPersistentModelIndex>
#include <QPointer>
/* Один layout для размеров, отрисовки, выделения и hit-test ссылок. */
static qreal layoutMessageText(QTextLayout& layout, int width, const QString& compactLink = {}) {
@@ -92,7 +93,6 @@ struct VoicePlayState {
bool loading = false;
bool playing = false;
bool paused = false;
qint64 startMs = 0;
float durationSec = 0.0f;
float pausedPosSec = 0.0f;
float displayFrac = 0.0f;
@@ -111,19 +111,17 @@ static void ensureVoiceTimer() {
bool anyActive = false;
SoundManager* sm = SoundManager::instance();
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) {
if (it->loading) { anyActive = true; continue; }
if (!it->playing && !it->paused) {
if (it->displayFrac > 0.001f || it->displayElapsed > 0.001f) anyActive = true;
continue;
}
if (it->paused) { anyActive = true; continue; }
if (!sm->isPcmPlaying(it->playbackId)) {
it->playing = false;
it->paused = false;
anyActive = true;
} else {
anyActive = true;
}
if (it->loading && !VoicePlayer::isPreparing()) it->loading = false;
if (it->playing && !sm->isPcmPlaying(it->playbackId)) it->playing = false;
float elapsed = it->paused ? it->pausedPosSec : 0;
const int rate = sm->pcmSampleRate(it->playbackId);
if (it->playing && rate > 0) elapsed = float(double(sm->pcmCursor(it->playbackId)) / rate);
elapsed = qBound(0.0f, elapsed, it->durationSec);
const float fraction = it->durationSec > 0 ? elapsed / it->durationSec : 0;
const bool moving = std::fabs(fraction - it->displayFrac) > 0.001f || std::fabs(elapsed - it->displayElapsed) > 0.001f;
it->displayFrac = moving ? it->displayFrac + (fraction - it->displayFrac) * 0.15f : fraction;
it->displayElapsed = moving ? it->displayElapsed + (elapsed - it->displayElapsed) * 0.15f : elapsed;
anyActive |= it->loading || it->playing || moving;
}
if (!anyActive) s_voiceTimer->stop();
if (s_voiceView) s_voiceView->viewport()->update();
@@ -288,7 +286,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
float clickFrac = (float)(localPos.x() - L.voiceWaveformRect.x()) / L.voiceWaveformRect.width();
if (clickFrac < 0) clickFrac = 0; if (clickFrac > 1) clickFrac = 1;
float targetSec = clickFrac * dur;
qint64 now = QDateTime::currentMSecsSinceEpoch();
if (!st.playing && !st.paused) {
/* Start playback then seek */
@@ -300,7 +297,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
MessageDelegate::startAutoPlay(filePath, actualDur, playbackId);
float position = clickFrac * actualDur;
auto& state = s_voiceStates[filePath];
state.startMs -= (qint64)(position * 1000); state.pausedPosSec = position;
state.pausedPosSec = position;
state.displayFrac = clickFrac; state.displayElapsed = position;
int rate = SoundManager::instance()->pcmSampleRate(playbackId);
if (rate > 0) SoundManager::instance()->seekPcm(playbackId, (unsigned long long)(position * rate));
@@ -309,7 +306,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
s_voiceTimer->start();
} else {
/* Currently playing or paused — seek */
st.startMs = now - (qint64)(targetSec * 1000.0);
if (st.paused)
st.pausedPosSec = targetSec;
else if (!st.playing)
@@ -332,8 +328,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
if (st.playing) {
/* Playing → Pause */
qint64 now = QDateTime::currentMSecsSinceEpoch();
st.pausedPosSec = (now - st.startMs) / 1000.0f;
const int rate = SoundManager::instance()->pcmSampleRate(st.playbackId);
st.pausedPosSec = rate > 0 ? float(double(SoundManager::instance()->pcmCursor(st.playbackId)) / rate) : 0;
st.playing = false;
st.paused = true;
SoundManager::instance()->pausePcm(st.playbackId);
@@ -345,8 +341,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
/* Paused → Resume */
st.paused = false;
st.playing = true;
qint64 now = QDateTime::currentMSecsSinceEpoch();
st.startMs = now - (qint64)(st.pausedPosSec * 1000.0);
st.displayFrac = (dur > 0) ? (st.pausedPosSec / dur) : 0;
st.displayElapsed = st.pausedPosSec;
SoundManager::instance()->resumePcm(st.playbackId);
@@ -1108,29 +1102,11 @@ void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& o
bool isPaused = st->paused;
/* compute elapsed/frac for progress */
float elapsed = 0, targetFrac = 0, durSec = st->durationSec;
float durSec = st->durationSec;
if (durSec <= 0) {
QString ds = index.data(MsgVoiceDurationRole).toString();
ds.remove('s'); durSec = ds.toFloat();
}
if (isPaused) {
elapsed = st->pausedPosSec;
} else if (isPlaying) {
SoundManager* sm = SoundManager::instance();
if (sm->isPcmPlaying(st->playbackId)) {
int sr = sm->pcmSampleRate(st->playbackId);
if (sr > 0) elapsed = (float)((double)sm->pcmCursor(st->playbackId) / sr);
}
if (elapsed <= 0) {
qint64 now = QDateTime::currentMSecsSinceEpoch();
elapsed = (now - st->startMs) / 1000.0f;
}
if (elapsed > durSec) elapsed = durSec;
}
targetFrac = (durSec > 0) ? (elapsed / durSec) : 0;
if (targetFrac < 0) targetFrac = 0; if (targetFrac > 1) targetFrac = 1;
st->displayFrac += (targetFrac - st->displayFrac) * 0.15f;
st->displayElapsed += (elapsed - st->displayElapsed) * 0.15f;
float frac = st->displayFrac;
QRectF playR = L.voicePlayBtnRect;
@@ -1714,14 +1690,14 @@ void MessageDelegate::drawAvatar(QPainter *p, const Layout &L,
}
void MessageDelegate::startAutoPlay(const QString& filePath, float actualDurationSec, uint64_t playbackId) {
if (actualDurationSec <= 0) return;
if (actualDurationSec <= 0 || !playbackId) { GUI_WARN("voice delegate: invalid playback duration/id"); return; }
ensureVoiceTimer();
s_voiceStates.clear();
VoicePlayState& st = s_voiceStates[filePath];
st.playbackId = playbackId;
st.playing = true;
st.paused = false;
st.durationSec = (actualDurationSec > 0) ? actualDurationSec : 1.0f;
st.startMs = QDateTime::currentMSecsSinceEpoch();
st.pausedPosSec = 0.0f;
st.displayFrac = 0.0f;
st.displayElapsed = 0.0f;