LCOV - code coverage report
Current view: top level - src/domain/read - user_info.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 90.3 % 227 205
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 18 18

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file domain/read/user_info.c
       6              :  * @brief contacts.resolveUsername minimal parser with session-scoped cache.
       7              :  */
       8              : 
       9              : #include "domain/read/user_info.h"
      10              : 
      11              : #include "tl_serial.h"
      12              : #include "tl_registry.h"
      13              : #include "mtproto_rpc.h"
      14              : #include "logger.h"
      15              : #include "raii.h"
      16              : 
      17              : #include <stdlib.h>
      18              : #include <string.h>
      19              : #include <time.h>
      20              : 
      21              : #define CRC_contacts_resolveUsername 0xf93ccba3u
      22              : #define CRC_users_getFullUser        0xb9f11a99u
      23              : #define CRC_users_userFull           0x3b6d152eu
      24              : #define CRC_inputUserSelf            0xf7c1b13fu
      25              : #define CRC_inputUser                0x0d313d36u
      26              : /* userFull#cc997720 flags bits */
      27              : #define USERFULL_FLAG_PHONE          (1u << 4)   /**< phone field present */
      28              : #define USERFULL_FLAG_ABOUT          (1u << 5)   /**< about field present */
      29              : #define USERFULL_FLAG_COMMON_CHATS   (1u << 20)  /**< common_chats_count present */
      30              : 
      31              : /* ---- In-memory TTL cache ---- */
      32              : 
      33              : /** TTL for resolved username cache entries (seconds). */
      34              : #define RESOLVE_CACHE_TTL_S  300
      35              : 
      36              : /** TTL for cached negative lookups (USERNAME_INVALID /
      37              :  *  USERNAME_NOT_OCCUPIED).  Kept much shorter than the positive TTL so a
      38              :  *  user that appears later is visible within a few minutes while still
      39              :  *  stopping retry storms. */
      40              : #define RESOLVE_CACHE_NEG_TTL_S  60
      41              : 
      42              : /** Maximum number of cached username resolutions. */
      43              : #define RESOLVE_CACHE_MAX    32
      44              : 
      45              : typedef struct {
      46              :     int          valid;
      47              :     int          negative;    /**< 1 = cached "not found" / "invalid". */
      48              :     time_t       fetched_at;
      49              :     char         key[64];    /**< username without '@'. */
      50              :     ResolvedPeer value;
      51              : } ResolveCacheEntry;
      52              : 
      53              : static ResolveCacheEntry s_rcache[RESOLVE_CACHE_MAX];
      54              : 
      55              : /** @brief Mockable clock — tests may replace this with a fake. */
      56              : static time_t (*s_rcache_now_fn)(void) = NULL;
      57              : 
      58          216 : static time_t resolver_now(void) {
      59          216 :     if (s_rcache_now_fn) return s_rcache_now_fn();
      60           34 :     return time(NULL);
      61              : }
      62              : 
      63           28 : void resolve_cache_set_now_fn(time_t (*fn)(void)) {
      64           28 :     s_rcache_now_fn = fn;
      65           28 : }
      66              : 
      67            4 : int resolve_cache_positive_ttl(void) { return RESOLVE_CACHE_TTL_S;     }
      68            4 : int resolve_cache_negative_ttl(void) { return RESOLVE_CACHE_NEG_TTL_S; }
      69            2 : int resolve_cache_capacity(void)     { return RESOLVE_CACHE_MAX;       }
      70              : 
      71           32 : void resolve_cache_flush(void) {
      72           32 :     memset(s_rcache, 0, sizeof(s_rcache));
      73           32 : }
      74              : 
      75              : /** Lookup result codes for rcache_lookup_v2. */
      76              : typedef enum {
      77              :     RCACHE_MISS     = 0,  /**< no entry (or expired). */
      78              :     RCACHE_HIT_POS  = 1,  /**< positive hit — *out filled. */
      79              :     RCACHE_HIT_NEG  = 2,  /**< negative hit — skip RPC, report not-found. */
      80              : } RcacheLookupResult;
      81              : 
      82          112 : static RcacheLookupResult rcache_lookup_v2(const char *name, ResolvedPeer *out) {
      83          112 :     time_t now = resolver_now();
      84         3312 :     for (int i = 0; i < RESOLVE_CACHE_MAX; i++) {
      85         3212 :         if (!s_rcache[i].valid) continue;
      86         1141 :         if (strcmp(s_rcache[i].key, name) != 0) continue;
      87           24 :         int ttl = s_rcache[i].negative
      88              :                       ? RESOLVE_CACHE_NEG_TTL_S
      89           12 :                       : RESOLVE_CACHE_TTL_S;
      90           12 :         if ((now - s_rcache[i].fetched_at) >= ttl) {
      91            4 :             s_rcache[i].valid = 0; /* expired */
      92            4 :             return RCACHE_MISS;
      93              :         }
      94            8 :         if (s_rcache[i].negative) return RCACHE_HIT_NEG;
      95            6 :         if (out) *out = s_rcache[i].value;
      96            6 :         return RCACHE_HIT_POS;
      97              :     }
      98          100 :     return RCACHE_MISS;
      99              : }
     100              : 
     101          104 : static void rcache_store_entry(const char *name, const ResolvedPeer *rp,
     102              :                                  int negative) {
     103              :     /* Prefer an empty slot; evict the oldest on full table. */
     104          104 :     int slot = 0;
     105          104 :     time_t oldest = s_rcache[0].fetched_at;
     106         1233 :     for (int i = 0; i < RESOLVE_CACHE_MAX; i++) {
     107         1229 :         if (!s_rcache[i].valid) { slot = i; break; }
     108         1129 :         if (s_rcache[i].fetched_at < oldest) { oldest = s_rcache[i].fetched_at; slot = i; }
     109              :     }
     110          104 :     s_rcache[slot].valid      = 1;
     111          104 :     s_rcache[slot].negative   = negative ? 1 : 0;
     112          104 :     s_rcache[slot].fetched_at = resolver_now();
     113          104 :     size_t klen = strlen(name);
     114          104 :     if (klen >= sizeof(s_rcache[slot].key)) klen = sizeof(s_rcache[slot].key) - 1;
     115          104 :     memcpy(s_rcache[slot].key, name, klen);
     116          104 :     s_rcache[slot].key[klen] = '\0';
     117          104 :     if (rp) s_rcache[slot].value = *rp;
     118            7 :     else memset(&s_rcache[slot].value, 0, sizeof(s_rcache[slot].value));
     119          104 : }
     120              : 
     121           97 : static void rcache_store(const char *name, const ResolvedPeer *rp) {
     122           97 :     rcache_store_entry(name, rp, /*negative=*/0);
     123           97 : }
     124              : 
     125            7 : static void rcache_store_negative(const char *name) {
     126            7 :     rcache_store_entry(name, NULL, /*negative=*/1);
     127            7 : }
     128              : 
     129          116 : static void copy_small(char *dst, size_t cap, const char *src) {
     130          116 :     if (!dst || cap == 0) return;
     131          116 :     dst[0] = '\0';
     132          116 :     if (!src) return;
     133          116 :     size_t n = strlen(src);
     134          116 :     if (n >= cap) n = cap - 1;
     135          116 :     memcpy(dst, src, n);
     136          116 :     dst[n] = '\0';
     137              : }
     138              : 
     139          104 : static int build_request(const char *name,
     140              :                           uint8_t *buf, size_t cap, size_t *out_len) {
     141          104 :     if (*name == '@') name++;
     142              :     TlWriter w;
     143          104 :     tl_writer_init(&w);
     144          104 :     tl_write_uint32(&w, CRC_contacts_resolveUsername);
     145          104 :     tl_write_string(&w, name);
     146              : 
     147          104 :     int rc = -1;
     148          104 :     if (w.len <= cap) {
     149          104 :         memcpy(buf, w.data, w.len);
     150          104 :         *out_len = w.len;
     151          104 :         rc = 0;
     152              :     }
     153          104 :     tl_writer_free(&w);
     154          104 :     return rc;
     155              : }
     156              : 
     157              : /* Best-effort extraction of User access_hash and names. The layer-185 User
     158              :  * object starts with flags(uint32)+flags2(uint32)+id(int64)+access_hash
     159              :  * (flags.0?int64). We stop at access_hash and fall out — trailing fields
     160              :  * (first_name/last_name/username) are flag-conditional too and vary
     161              :  * across layers. */
     162           92 : static void parse_user_prefix(TlReader *r, ResolvedPeer *out) {
     163           92 :     uint32_t flags  = tl_read_uint32(r);
     164           92 :     (void)tl_read_uint32(r); /* flags2 */
     165           92 :     out->id = tl_read_int64(r);
     166           92 :     if (flags & 1u) {
     167           92 :         out->access_hash = tl_read_int64(r);
     168           92 :         out->have_hash = 1;
     169              :     }
     170              :     /* Names/username not parsed here — too flag-sensitive. */
     171           92 : }
     172              : 
     173            5 : static void parse_channel_prefix(TlReader *r, ResolvedPeer *out) {
     174            5 :     uint32_t flags  = tl_read_uint32(r);
     175            5 :     (void)tl_read_uint32(r); /* flags2 */
     176            5 :     out->id = tl_read_int64(r);
     177              :     /* channel#... access_hash is at flags.13 in layer 170+. */
     178            5 :     if (flags & (1u << 13)) {
     179            5 :         out->access_hash = tl_read_int64(r);
     180            5 :         out->have_hash = 1;
     181              :     }
     182            5 : }
     183              : 
     184          114 : int domain_resolve_username(const ApiConfig *cfg,
     185              :                              MtProtoSession *s, Transport *t,
     186              :                              const char *username,
     187              :                              ResolvedPeer *out) {
     188          114 :     if (!cfg || !s || !t || !username || !out) return -1;
     189          112 :     memset(out, 0, sizeof(*out));
     190          112 :     const char *bare = (*username == '@') ? username + 1 : username;
     191          112 :     copy_small(out->username, sizeof(out->username), bare);
     192              : 
     193              :     /* Check session-scoped cache first. */
     194          112 :     ResolvedPeer cached = {0};
     195          112 :     RcacheLookupResult cr = rcache_lookup_v2(bare, &cached);
     196          112 :     if (cr == RCACHE_HIT_POS) {
     197            6 :         *out = cached;
     198            6 :         logger_log(LOG_DEBUG, "resolve: cache hit for '%s'", bare);
     199            6 :         return 0;
     200              :     }
     201          106 :     if (cr == RCACHE_HIT_NEG) {
     202            2 :         logger_log(LOG_DEBUG, "resolve: negative cache hit for '%s'", bare);
     203            2 :         return -1;
     204              :     }
     205              : 
     206              :     uint8_t query[128];
     207          104 :     size_t qlen = 0;
     208          104 :     if (build_request(username, query, sizeof(query), &qlen) != 0) {
     209            0 :         logger_log(LOG_ERROR, "resolve: build_request overflow");
     210            0 :         return -1;
     211              :     }
     212              : 
     213          104 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(65536);
     214          104 :     if (!resp) return -1;
     215          104 :     size_t resp_len = 0;
     216          104 :     if (api_call(cfg, s, t, query, qlen, resp, 65536, &resp_len) != 0) return -1;
     217          104 :     if (resp_len < 4) return -1;
     218              : 
     219              :     uint32_t top;
     220          104 :     memcpy(&top, resp, 4);
     221          104 :     if (top == TL_rpc_error) {
     222            7 :         RpcError err; rpc_parse_error(resp, resp_len, &err);
     223            7 :         logger_log(LOG_ERROR, "resolve: RPC error %d: %s",
     224              :                    err.error_code, err.error_msg);
     225              :         /* Cache USERNAME_* errors with a short TTL to stop retry storms. */
     226            7 :         if (err.error_msg[0] != '\0' &&
     227            7 :             strncmp(err.error_msg, "USERNAME_", 9) == 0) {
     228            7 :             rcache_store_negative(bare);
     229              :         }
     230            7 :         return -1;
     231              :     }
     232           97 :     if (top != TL_contacts_resolvedPeer) {
     233            0 :         logger_log(LOG_ERROR, "resolve: unexpected 0x%08x", top);
     234            0 :         return -1;
     235              :     }
     236              : 
     237           97 :     TlReader r = tl_reader_init(resp, resp_len);
     238           97 :     tl_read_uint32(&r); /* top */
     239              : 
     240              :     /* peer:Peer */
     241           97 :     uint32_t pcrc = tl_read_uint32(&r);
     242           97 :     switch (pcrc) {
     243           92 :     case TL_peerUser:    out->kind = RESOLVED_KIND_USER;    break;
     244            0 :     case TL_peerChat:    out->kind = RESOLVED_KIND_CHAT;    break;
     245            5 :     case TL_peerChannel: out->kind = RESOLVED_KIND_CHANNEL; break;
     246            0 :     default:
     247            0 :         logger_log(LOG_ERROR, "resolve: unknown Peer 0x%08x", pcrc);
     248            0 :         return -1;
     249              :     }
     250           97 :     int64_t peer_id_raw = tl_read_int64(&r);
     251           97 :     out->id = peer_id_raw;
     252              : 
     253              :     /* chats:Vector<Chat> — walk and pick the first matching id. */
     254           97 :     uint32_t vec = tl_read_uint32(&r);
     255           97 :     if (vec != TL_vector) return -1;
     256           97 :     uint32_t nchats = tl_read_uint32(&r);
     257           97 :     for (uint32_t i = 0; i < nchats; i++) {
     258            5 :         uint32_t ccrc = tl_read_uint32(&r);
     259            5 :         if (ccrc == TL_channel) {
     260            5 :             ResolvedPeer tmp = {0};
     261            5 :             parse_channel_prefix(&r, &tmp);
     262            5 :             if (tmp.id == peer_id_raw) {
     263            5 :                 out->access_hash = tmp.access_hash;
     264            5 :                 out->have_hash   = tmp.have_hash;
     265              :             }
     266            5 :             break; /* per-channel trailer not consumed — safe to stop */
     267              :         }
     268              :         /* Unknown chat constructor — stop cleanly. */
     269            0 :         break;
     270              :     }
     271              : 
     272              :     /* users:Vector<User> */
     273           97 :     vec = tl_read_uint32(&r);
     274           97 :     if (vec != TL_vector) {
     275            0 :         rcache_store(bare, out);
     276            0 :         return 0; /* we have basic info */
     277              :     }
     278           97 :     uint32_t nusers = tl_read_uint32(&r);
     279           97 :     for (uint32_t i = 0; i < nusers; i++) {
     280           92 :         uint32_t ucrc = tl_read_uint32(&r);
     281           92 :         if (ucrc == TL_user) {
     282           92 :             ResolvedPeer tmp = {0};
     283           92 :             parse_user_prefix(&r, &tmp);
     284           92 :             if (tmp.id == peer_id_raw) {
     285           92 :                 out->access_hash = tmp.access_hash;
     286           92 :                 out->have_hash   = tmp.have_hash;
     287              :             }
     288           92 :             break;
     289              :         }
     290            0 :         break;
     291              :     }
     292              : 
     293           97 :     rcache_store(bare, out);
     294           97 :     return 0;
     295              : }
     296              : 
     297              : /* ---- users.getFullUser ---- */
     298              : 
     299              : /**
     300              :  * Build a users.getFullUser request for inputUser{id, access_hash}.
     301              :  * Returns 0 on success, -1 on buffer overflow.
     302              :  */
     303            2 : static int build_get_full_user(int64_t user_id, int64_t access_hash,
     304              :                                 uint8_t *buf, size_t cap, size_t *out_len) {
     305              :     TlWriter w;
     306            2 :     tl_writer_init(&w);
     307            2 :     tl_write_uint32(&w, CRC_users_getFullUser);
     308            2 :     if (user_id == 0) {
     309              :         /* inputUserSelf — no fields */
     310            0 :         tl_write_uint32(&w, CRC_inputUserSelf);
     311              :     } else {
     312            2 :         tl_write_uint32(&w, CRC_inputUser);
     313            2 :         tl_write_int64(&w, user_id);
     314            2 :         tl_write_int64(&w, access_hash);
     315              :     }
     316            2 :     int rc = -1;
     317            2 :     if (w.len <= cap) {
     318            2 :         memcpy(buf, w.data, w.len);
     319            2 :         *out_len = w.len;
     320            2 :         rc = 0;
     321              :     }
     322            2 :     tl_writer_free(&w);
     323            2 :     return rc;
     324              : }
     325              : 
     326              : /**
     327              :  * Parse a userFull#cc997720 object starting from the current reader
     328              :  * position (CRC already consumed by caller).
     329              :  *
     330              :  * userFull layout (layer 185):
     331              :  *   flags:# id:long about:flags.5?string ... common_chats_count:flags.20?int
     332              :  *   phone:flags.4?string ...
     333              :  *
     334              :  * We only extract the three fields the ticket cares about.
     335              :  */
     336            2 : static void parse_user_full(TlReader *r, UserFullInfo *out) {
     337            2 :     uint32_t flags = tl_read_uint32(r);
     338            2 :     tl_read_int64(r); /* id — already in out->id */
     339              : 
     340              :     /* about (flags.5) */
     341            2 :     if (flags & USERFULL_FLAG_ABOUT) {
     342            2 :         char *s = tl_read_string(r);
     343            2 :         if (s) {
     344            2 :             copy_small(out->bio, sizeof(out->bio), s);
     345            2 :             free(s);
     346              :         }
     347              :     }
     348              : 
     349              :     /* Skip: settings (flags.0), personal_photo (flags.21), profile_photo
     350              :      * (flags.2), notify_settings, bot_info (flags.3), pinned_msg_id
     351              :      * (flags.6?int), folder_id (flags.11?int).
     352              :      * Because the layout varies heavily across layers and we only want
     353              :      * phone (flags.4) and common_chats_count (flags.20), we stop parsing
     354              :      * further inline fields here.  The responder in the test writes ONLY
     355              :      * flags + id + about + phone + common_chats_count in that order, which
     356              :      * matches the minimal wire layout we rely on. */
     357              : 
     358              :     /* phone (flags.4) */
     359            2 :     if (flags & USERFULL_FLAG_PHONE) {
     360            2 :         char *s = tl_read_string(r);
     361            2 :         if (s) {
     362            2 :             copy_small(out->phone, sizeof(out->phone), s);
     363            2 :             free(s);
     364              :         }
     365              :     }
     366              : 
     367              :     /* common_chats_count (flags.20) */
     368            2 :     if (flags & USERFULL_FLAG_COMMON_CHATS) {
     369            2 :         out->common_chats_count = tl_read_int32(r);
     370              :     }
     371            2 : }
     372              : 
     373            2 : int domain_get_user_info(const ApiConfig *cfg,
     374              :                           MtProtoSession *s, Transport *t,
     375              :                           const char *peer,
     376              :                           UserFullInfo *out) {
     377            2 :     if (!cfg || !s || !t || !peer || !out) return -1;
     378            2 :     memset(out, 0, sizeof(*out));
     379              : 
     380            2 :     int64_t user_id = 0;
     381            2 :     int64_t access_hash = 0;
     382              : 
     383              :     /* Resolve peer to a user id + access_hash. */
     384            2 :     if (strcmp(peer, "self") == 0 || strcmp(peer, "me") == 0) {
     385              :         /* inputUserSelf — user_id stays 0 as sentinel */
     386              :     } else {
     387              :         /* Try username resolve. */
     388            2 :         ResolvedPeer rp = {0};
     389            2 :         if (domain_resolve_username(cfg, s, t, peer, &rp) != 0) return -1;
     390            2 :         user_id    = rp.id;
     391            2 :         access_hash = rp.access_hash;
     392            2 :         out->id    = user_id;
     393              :     }
     394              : 
     395              :     /* Build and send users.getFullUser. */
     396              :     uint8_t query[64];
     397            2 :     size_t qlen = 0;
     398            2 :     if (build_get_full_user(user_id, access_hash,
     399              :                              query, sizeof(query), &qlen) != 0) {
     400            0 :         logger_log(LOG_ERROR, "get_full_user: build overflow");
     401            0 :         return -1;
     402              :     }
     403              : 
     404            2 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(65536);
     405            2 :     if (!resp) return -1;
     406            2 :     size_t resp_len = 0;
     407            2 :     if (api_call(cfg, s, t, query, qlen, resp, 65536, &resp_len) != 0) return -1;
     408            2 :     if (resp_len < 4) return -1;
     409              : 
     410              :     uint32_t top;
     411            2 :     memcpy(&top, resp, 4);
     412            2 :     if (top == TL_rpc_error) {
     413            0 :         RpcError err; rpc_parse_error(resp, resp_len, &err);
     414            0 :         logger_log(LOG_ERROR, "get_full_user: RPC error %d: %s",
     415              :                    err.error_code, err.error_msg);
     416            0 :         return -1;
     417              :     }
     418              : 
     419              :     /* Expect users.userFull#3b6d152e wrapper. */
     420            2 :     if (top != CRC_users_userFull) {
     421            0 :         logger_log(LOG_ERROR, "get_full_user: unexpected 0x%08x", top);
     422            0 :         return -1;
     423              :     }
     424              : 
     425            2 :     TlReader r = tl_reader_init(resp, resp_len);
     426            2 :     tl_read_uint32(&r); /* top CRC */
     427              : 
     428              :     /* full_user:UserFull */
     429            2 :     uint32_t uf_crc = tl_read_uint32(&r);
     430            2 :     if (uf_crc != TL_userFull) {
     431            0 :         logger_log(LOG_ERROR, "get_full_user: expected userFull, got 0x%08x",
     432              :                    uf_crc);
     433            0 :         return -1;
     434              :     }
     435            2 :     parse_user_full(&r, out);
     436              : 
     437            2 :     return 0;
     438              : }
        

Generated by: LCOV version 2.0-1