LCOV - code coverage report
Current view: top level - src/domain/read - dialogs.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 78.4 % 222 174
Test Date: 2026-05-06 13:17:08 Functions: 87.5 % 8 7

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file domain/read/dialogs.c
       6              :  * @brief messages.getDialogs parser (minimal v1).
       7              :  */
       8              : 
       9              : #include "domain/read/dialogs.h"
      10              : 
      11              : #include "tl_serial.h"
      12              : #include "tl_registry.h"
      13              : #include "tl_skip.h"
      14              : #include "mtproto_rpc.h"
      15              : #include "logger.h"
      16              : #include "raii.h"
      17              : 
      18              : #include <stddef.h>
      19              : #include <time.h>
      20              : 
      21              : #include <stdlib.h>
      22              : #include <string.h>
      23              : 
      24              : /* ---- In-memory TTL cache ---- */
      25              : 
      26              : /** Default TTL for the dialogs cache (seconds). */
      27              : #ifndef DIALOGS_CACHE_TTL_S
      28              : #define DIALOGS_CACHE_TTL_S 60
      29              : #endif
      30              : 
      31              : /** Maximum cached dialogs per call site (archived=0 and archived=1). */
      32              : #define DIALOGS_CACHE_SLOTS 2
      33              : #define DIALOGS_CACHE_MAX   512
      34              : 
      35              : typedef struct {
      36              :     int         valid;
      37              :     int         archived;
      38              :     time_t      fetched_at;
      39              :     int         count;
      40              :     int         total_count;
      41              :     DialogEntry entries[DIALOGS_CACHE_MAX];
      42              : } DialogsCache;
      43              : 
      44              : static DialogsCache s_cache[DIALOGS_CACHE_SLOTS]; /* [0]=inbox [1]=archive */
      45              : 
      46              : /** @brief Mockable clock — tests may replace this with a fake. */
      47              : static time_t (*s_now_fn)(void) = NULL;
      48              : 
      49           43 : static time_t dialogs_now(void) {
      50           43 :     if (s_now_fn) return s_now_fn();
      51           38 :     return time(NULL);
      52              : }
      53              : 
      54              : /**
      55              :  * @brief Override the clock used for TTL checks (test use only).
      56              :  * Pass NULL to restore the real clock.
      57              :  */
      58           16 : void dialogs_cache_set_now_fn(time_t (*fn)(void)) {
      59           16 :     s_now_fn = fn;
      60           16 : }
      61              : 
      62              : /**
      63              :  * @brief Flush the in-memory dialogs cache (test use only).
      64              :  *
      65              :  * Call before each unit test that drives domain_get_dialogs directly so
      66              :  * that cached state from a previous test does not mask a fresh RPC.
      67              :  */
      68           21 : void dialogs_cache_flush(void) {
      69           21 :     memset(s_cache, 0, sizeof(s_cache));
      70           21 : }
      71              : 
      72              : 
      73              : #define CRC_messages_getDialogs 0xa0f4cb4fu
      74              : #define CRC_inputPeerEmpty      0x7f3b18eau
      75              : 
      76              : /* ---- Request builder ---- */
      77              : 
      78              : /* flags bit for the optional folder_id field in messages.getDialogs */
      79              : #define GETDIALOGS_FLAG_FOLDER_ID (1u << 1)
      80              : 
      81           23 : static int build_request(int limit, int archived,
      82              :                          uint8_t *buf, size_t cap, size_t *out_len) {
      83              :     TlWriter w;
      84           23 :     tl_writer_init(&w);
      85           23 :     tl_write_uint32(&w, CRC_messages_getDialogs);
      86           23 :     uint32_t flags = archived ? GETDIALOGS_FLAG_FOLDER_ID : 0u;
      87           23 :     tl_write_uint32(&w, flags);                   /* flags */
      88           23 :     if (archived)
      89            2 :         tl_write_int32(&w, 1);                    /* folder_id = 1 (Archive) */
      90           23 :     tl_write_int32 (&w, 0);                       /* offset_date */
      91           23 :     tl_write_int32 (&w, 0);                       /* offset_id */
      92           23 :     tl_write_uint32(&w, CRC_inputPeerEmpty);      /* offset_peer */
      93           23 :     tl_write_int32 (&w, limit);                   /* limit */
      94           23 :     tl_write_int64 (&w, 0);                       /* hash */
      95              : 
      96           23 :     int rc = -1;
      97           23 :     if (w.len <= cap) {
      98           23 :         memcpy(buf, w.data, w.len);
      99           23 :         *out_len = w.len;
     100           23 :         rc = 0;
     101              :     }
     102           23 :     tl_writer_free(&w);
     103           23 :     return rc;
     104              : }
     105              : 
     106              : /* ---- Dialog TL parsing (minimal, schema-tolerant) ----
     107              :  *
     108              :  * dialog#d58a08c6 flags:# pinned:flags.2?true unread_mark:flags.3?true
     109              :  *     view_forum_as_messages:flags.6?true
     110              :  *     peer:Peer
     111              :  *     top_message:int
     112              :  *     read_inbox_max_id:int
     113              :  *     read_outbox_max_id:int
     114              :  *     unread_count:int
     115              :  *     unread_mentions_count:int
     116              :  *     unread_reactions_count:int
     117              :  *     notify_settings:PeerNotifySettings
     118              :  *     pts:flags.0?int
     119              :  *     draft:flags.1?DraftMessage
     120              :  *     folder_id:flags.4?int
     121              :  *     ttl_period:flags.5?int
     122              :  *     = Dialog
     123              :  *
     124              :  * We only pull the first block (peer + unread_count + top_message_id).
     125              :  * Optional trailing fields are not read because callers don't need them;
     126              :  * the reader stops advancing after this function returns.
     127              :  */
     128              : #define CRC_dialog       0xd58a08c6u
     129              : #define CRC_dialogFolder 0x71bd134cu
     130              : 
     131              : /* Skip a dialogFolder entry after its CRC has been consumed.
     132              :  * Layout: flags(uint32) peer:Peer top_message(int32) + 4×int32 counts.
     133              :  * pinned:flags.2?true is a flag bit, not a wire field. */
     134            1 : static int skip_dialog_folder(TlReader *r) {
     135            1 :     tl_read_uint32(r); /* flags */
     136            1 :     if (tl_skip_peer(r) != 0) return -1;
     137            0 :     for (int k = 0; k < 5; k++) {
     138            0 :         if (r->len - r->pos < 4) return -1;
     139            0 :         tl_read_int32(r);
     140              :     }
     141            0 :     return 0;
     142              : }
     143              : 
     144          418 : static int parse_peer(TlReader *r, DialogEntry *out) {
     145          418 :     if (!tl_reader_ok(r)) return -1;
     146          418 :     uint32_t crc = tl_read_uint32(r);
     147          418 :     switch (crc) {
     148          416 :     case TL_peerUser:    out->kind = DIALOG_PEER_USER;    break;
     149            1 :     case TL_peerChat:    out->kind = DIALOG_PEER_CHAT;    break;
     150            1 :     case TL_peerChannel: out->kind = DIALOG_PEER_CHANNEL; break;
     151            0 :     default:
     152            0 :         logger_log(LOG_WARN, "dialogs: unknown Peer constructor 0x%08x", crc);
     153            0 :         out->kind = DIALOG_PEER_UNKNOWN;
     154            0 :         return -1;
     155              :     }
     156          418 :     out->peer_id = tl_read_int64(r);
     157          418 :     return 0;
     158              : }
     159              : 
     160           25 : int domain_get_dialogs(const ApiConfig *cfg,
     161              :                        MtProtoSession *s, Transport *t,
     162              :                        int max_entries, int archived,
     163              :                        DialogEntry *out, int *out_count,
     164              :                        int *total_count) {
     165           25 :     if (!cfg || !s || !t || !out || !out_count || max_entries <= 0) return -1;
     166           25 :     *out_count = 0;
     167           25 :     if (total_count) *total_count = 0;
     168              : 
     169              :     /* ---- TTL cache check ---- */
     170           25 :     int slot = archived ? 1 : 0;
     171           25 :     DialogsCache *cache = &s_cache[slot];
     172           25 :     time_t now = dialogs_now();
     173           25 :     if (cache->valid && (now - cache->fetched_at) < DIALOGS_CACHE_TTL_S) {
     174            2 :         int n = cache->count < max_entries ? cache->count : max_entries;
     175            2 :         memcpy(out, cache->entries, (size_t)n * sizeof(DialogEntry));
     176            2 :         *out_count = n;
     177            2 :         if (total_count) *total_count = cache->total_count;
     178            2 :         logger_log(LOG_DEBUG, "dialogs: served %d entries from cache (age=%lds)",
     179            2 :                    n, (long)(now - cache->fetched_at));
     180            2 :         return 0;
     181              :     }
     182              : 
     183              :     uint8_t query[132];
     184           23 :     size_t qlen = 0;
     185           23 :     if (build_request(max_entries, archived, query, sizeof(query), &qlen) != 0) {
     186            0 :         logger_log(LOG_ERROR, "dialogs: build_request overflow");
     187            0 :         return -1;
     188              :     }
     189              : 
     190           23 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(262144);
     191           23 :     if (!resp) return -1;
     192           23 :     size_t resp_len = 0;
     193           23 :     if (api_call(cfg, s, t, query, qlen, resp, 262144, &resp_len) != 0) {
     194            0 :         logger_log(LOG_ERROR, "dialogs: api_call failed");
     195            0 :         return -1;
     196              :     }
     197           23 :     if (resp_len < 8) {
     198            0 :         logger_log(LOG_ERROR, "dialogs: response too short");
     199            0 :         return -1;
     200              :     }
     201              : 
     202              :     uint32_t top;
     203           23 :     memcpy(&top, resp, 4);
     204           23 :     if (top == TL_rpc_error) {
     205              :         RpcError err;
     206            1 :         rpc_parse_error(resp, resp_len, &err);
     207            1 :         logger_log(LOG_ERROR, "dialogs: RPC error %d: %s",
     208              :                    err.error_code, err.error_msg);
     209            1 :         return -1;
     210              :     }
     211              : 
     212              :     /* messages.dialogsNotModified#f0e3e596 count:int
     213              :      * Returned by the server when the client's hash matches the cached list —
     214              :      * no entries follow.  We surface count via total_count and return 0
     215              :      * dialogs so callers know the cache is valid. */
     216           22 :     if (top == TL_messages_dialogsNotModified) {
     217            2 :         TlReader r = tl_reader_init(resp, resp_len);
     218            2 :         tl_read_uint32(&r); /* skip constructor */
     219            2 :         int32_t srv_count = tl_read_int32(&r);
     220            2 :         if (total_count) *total_count = (int)srv_count;
     221            2 :         logger_log(LOG_DEBUG,
     222              :                    "dialogs: not-modified, server count=%d", srv_count);
     223            2 :         return 0; /* *out_count remains 0; caller should use its cache */
     224              :     }
     225              : 
     226           20 :     if (top != TL_messages_dialogs && top != TL_messages_dialogsSlice) {
     227            1 :         logger_log(LOG_ERROR,
     228              :                    "dialogs: unexpected top constructor 0x%08x", top);
     229            1 :         return -1;
     230              :     }
     231              : 
     232           19 :     TlReader r = tl_reader_init(resp, resp_len);
     233           19 :     tl_read_uint32(&r); /* top constructor */
     234              : 
     235              :     /* messages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog>... */
     236           19 :     if (top == TL_messages_dialogsSlice) {
     237            8 :         int32_t slice_total = tl_read_int32(&r);
     238            8 :         if (total_count) *total_count = (int)slice_total;
     239              :     }
     240              : 
     241              :     /* dialogs vector */
     242           19 :     uint32_t vec = tl_read_uint32(&r);
     243           19 :     if (vec != TL_vector) {
     244            1 :         logger_log(LOG_ERROR, "dialogs: expected Vector<Dialog>, got 0x%08x", vec);
     245            1 :         return -1;
     246              :     }
     247           18 :     uint32_t count = tl_read_uint32(&r);
     248              :     /* For the complete-list variant, total == the vector length. */
     249           18 :     if (top == TL_messages_dialogs && total_count) *total_count = (int)count;
     250           18 :     int written = 0;
     251           18 :     int parsed  = 0; /* entries fully consumed from the stream */
     252          436 :     for (uint32_t i = 0; i < count && written < max_entries; i++) {
     253          422 :         if (!tl_reader_ok(&r)) break;
     254          420 :         uint32_t dcrc = tl_read_uint32(&r);
     255          420 :         if (dcrc == CRC_dialogFolder) {
     256              :             /* Skip the archive-folder summary entry; it is not a real dialog.
     257              :              * Advance past it so the cursor stays aligned for the join. */
     258            1 :             if (skip_dialog_folder(&r) != 0) break;
     259            0 :             logger_log(LOG_DEBUG, "dialogs: skipped dialogFolder entry");
     260            0 :             parsed++;
     261            0 :             continue;
     262              :         }
     263          419 :         if (dcrc != CRC_dialog) {
     264            1 :             logger_log(LOG_WARN, "dialogs: unknown Dialog constructor 0x%08x",
     265              :                        dcrc);
     266            1 :             break;
     267              :         }
     268              : 
     269          418 :         uint32_t flags = tl_read_uint32(&r);
     270          418 :         DialogEntry e = {0};
     271          418 :         if (parse_peer(&r, &e) != 0) break;
     272          418 :         e.top_message_id = tl_read_int32(&r);
     273          418 :         tl_read_int32(&r); /* read_inbox_max_id */
     274          418 :         tl_read_int32(&r); /* read_outbox_max_id */
     275          418 :         e.unread_count = tl_read_int32(&r);
     276          418 :         tl_read_int32(&r); /* unread_mentions_count */
     277          418 :         tl_read_int32(&r); /* unread_reactions_count */
     278              : 
     279          418 :         if (tl_skip_peer_notify_settings(&r) != 0) {
     280            0 :             logger_log(LOG_WARN,
     281              :                        "dialogs: failed to skip PeerNotifySettings");
     282            0 :             out[written++] = e;
     283            0 :             parsed++;
     284            0 :             break;
     285              :         }
     286              : 
     287          418 :         if (flags & (1u << 0)) tl_read_int32(&r); /* pts */
     288          418 :         if (flags & (1u << 1)) {
     289            0 :             if (tl_skip_draft_message(&r) != 0) {
     290            0 :                 logger_log(LOG_WARN,
     291              :                            "dialogs: complex draft — stopping after entry %u",
     292              :                            i);
     293            0 :                 out[written++] = e;
     294            0 :                 parsed++;
     295            0 :                 break;
     296              :             }
     297              :         }
     298          418 :         if (flags & (1u << 4)) tl_read_int32(&r); /* folder_id */
     299          418 :         if (flags & (1u << 5)) tl_read_int32(&r); /* ttl_period */
     300              : 
     301          418 :         out[written++] = e;
     302          418 :         parsed++;
     303              :     }
     304              : 
     305           18 :     *out_count = written;
     306              : 
     307              :     /* ---- Populate cache after successful RPC (before title join) ----
     308              :      * The join adds titles in-place so we store after the join, but we
     309              :      * need to handle all exit paths.  Prime the cache fields now so that
     310              :      * the goto-jump at join_done always sees a consistent state. */
     311              :     {
     312           18 :         int cached_n = written < DIALOGS_CACHE_MAX ? written : DIALOGS_CACHE_MAX;
     313           18 :         cache->valid      = 1;
     314           18 :         cache->archived   = archived;
     315           18 :         cache->fetched_at = dialogs_now();
     316           18 :         cache->count      = cached_n;
     317           18 :         cache->total_count = total_count ? *total_count : written;
     318           18 :         memcpy(cache->entries, out, (size_t)cached_n * sizeof(DialogEntry));
     319              :     }
     320              : 
     321              :     /* ---- Title join ----
     322              :      *
     323              :      * messages.dialogs / dialogsSlice continues with:
     324              :      *   messages:Vector<Message>
     325              :      *   chats:Vector<Chat>
     326              :      *   users:Vector<User>
     327              :      *
     328              :      * Proceed only when every Dialog entry was fully consumed from the stream
     329              :      * (parsed == count); otherwise the cursor is mis-positioned and reading
     330              :      * the messages/chats/users vectors would produce garbage. */
     331           18 :     if (parsed < (int)count) {
     332            2 :         logger_log(LOG_WARN, "dialogs: partial parse (%d/%u) — skipping join",
     333              :                    parsed, count);
     334            2 :         return 0;
     335              :     }
     336           16 :     if (!tl_reader_ok(&r))    return 0;
     337              : 
     338           16 :     uint32_t mvec = tl_read_uint32(&r);
     339           16 :     if (mvec != TL_vector) {
     340            0 :         logger_log(LOG_WARN, "dialogs: expected messages Vector, got 0x%08x", mvec);
     341            0 :         return 0;
     342              :     }
     343           16 :     uint32_t mcount = tl_read_uint32(&r);
     344           16 :     for (uint32_t i = 0; i < mcount; i++) {
     345            0 :         if (tl_skip_message(&r) != 0) {
     346            0 :             logger_log(LOG_WARN, "dialogs: tl_skip_message failed at index %u "
     347              :                        "(pos=%zu) — skipping join", i, r.pos);
     348            0 :             return 0;
     349              :         }
     350              :     }
     351              : 
     352           16 :     uint32_t cvec = tl_read_uint32(&r);
     353           16 :     if (cvec != TL_vector) {
     354            0 :         logger_log(LOG_WARN, "dialogs: expected chats Vector, got 0x%08x", cvec);
     355            0 :         return 0;
     356              :     }
     357           16 :     uint32_t ccount = tl_read_uint32(&r);
     358           16 :     ChatSummary *chats = (ccount > 0)
     359            1 :         ? (ChatSummary *)calloc(ccount, sizeof(ChatSummary))
     360           16 :         : NULL;
     361           16 :     uint32_t chats_written = 0;
     362           17 :     for (uint32_t i = 0; i < ccount; i++) {
     363            1 :         ChatSummary cs = {0};
     364            1 :         if (tl_extract_chat(&r, &cs) != 0) {
     365            0 :             logger_log(LOG_WARN, "dialogs: tl_extract_chat failed at index %u", i);
     366            0 :             free(chats); chats = NULL; goto join_done;
     367              :         }
     368            1 :         if (chats) chats[chats_written++] = cs;
     369              :     }
     370              : 
     371           16 :     uint32_t uvec = tl_read_uint32(&r);
     372           16 :     if (uvec != TL_vector) {
     373            0 :         logger_log(LOG_WARN, "dialogs: expected users Vector, got 0x%08x", uvec);
     374            0 :         free(chats); goto join_done;
     375              :     }
     376           16 :     uint32_t ucount = tl_read_uint32(&r);
     377           16 :     UserSummary *users = (ucount > 0)
     378            4 :         ? (UserSummary *)calloc(ucount, sizeof(UserSummary))
     379           16 :         : NULL;
     380           16 :     uint32_t users_written = 0;
     381           20 :     for (uint32_t i = 0; i < ucount; i++) {
     382            4 :         UserSummary us = {0};
     383            4 :         if (tl_extract_user(&r, &us) != 0) {
     384            0 :             logger_log(LOG_WARN, "dialogs: tl_extract_user failed at index %u", i);
     385            0 :             free(chats); free(users); goto join_done;
     386              :         }
     387            4 :         if (users) users[users_written++] = us;
     388              :     }
     389              : 
     390              :     /* Fill DialogEntry title/username + access_hash by looking up peer_id. */
     391          432 :     for (int i = 0; i < *out_count; i++) {
     392          416 :         DialogEntry *e = &out[i];
     393          416 :         if (e->kind == DIALOG_PEER_USER) {
     394          414 :             for (uint32_t j = 0; j < users_written; j++) {
     395            4 :                 if (users[j].id == e->peer_id) {
     396            4 :                     memcpy(e->title,    users[j].name,     sizeof(e->title));
     397            4 :                     memcpy(e->username, users[j].username, sizeof(e->username));
     398            4 :                     e->access_hash      = users[j].access_hash;
     399            4 :                     e->have_access_hash = users[j].have_access_hash;
     400            4 :                     break;
     401              :                 }
     402              :             }
     403              :         } else { /* CHAT / CHANNEL */
     404            2 :             for (uint32_t j = 0; j < chats_written; j++) {
     405            1 :                 if (chats[j].id == e->peer_id) {
     406            1 :                     memcpy(e->title, chats[j].title, sizeof(e->title));
     407              :                     /* Legacy chat has no access_hash on the wire; leave
     408              :                      * have_access_hash=0 so the caller knows. Channels do. */
     409            1 :                     e->access_hash      = chats[j].access_hash;
     410            1 :                     e->have_access_hash = chats[j].have_access_hash;
     411            1 :                     break;
     412              :                 }
     413              :             }
     414              :         }
     415              :     }
     416           16 :     free(chats);
     417           16 :     free(users);
     418              :     /* Refresh cached entries to include joined titles / access_hashes. */
     419              :     {
     420           16 :         int cached_n = *out_count < DIALOGS_CACHE_MAX ? *out_count : DIALOGS_CACHE_MAX;
     421           16 :         memcpy(cache->entries, out, (size_t)cached_n * sizeof(DialogEntry));
     422              :     }
     423           16 : join_done:
     424           16 :     return 0;
     425              : }
     426              : 
     427            0 : int domain_dialogs_find_by_id(int64_t peer_id, DialogEntry *out) {
     428            0 :     for (int slot = 0; slot < DIALOGS_CACHE_SLOTS; slot++) {
     429            0 :         const DialogsCache *c = &s_cache[slot];
     430            0 :         if (!c->valid) continue;
     431            0 :         for (int i = 0; i < c->count; i++) {
     432            0 :             if (c->entries[i].peer_id == peer_id) {
     433            0 :                 if (out) *out = c->entries[i];
     434            0 :                 return 0;
     435              :             }
     436              :         }
     437              :     }
     438            0 :     return -1;
     439              : }
        

Generated by: LCOV version 2.0-1