LCOV - code coverage report
Current view: top level - src/domain/read - user_info.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 89.7 % 232 208
Test Date: 2026-05-06 13:17:06 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 << 0)) { out->access_hash = tl_read_int64(r); out->have_hash = 1; }
     167              : 
     168           92 :     char first[64] = {0}, last[64] = {0};
     169           92 :     if (flags & (1u << 1)) { RAII_STRING char *s = tl_read_string(r); copy_small(first, sizeof(first), s); }
     170           92 :     if (flags & (1u << 2)) { RAII_STRING char *s = tl_read_string(r); copy_small(last,  sizeof(last),  s); }
     171           92 :     if (flags & (1u << 3)) { RAII_STRING char *s = tl_read_string(r); copy_small(out->username, sizeof(out->username), s); }
     172              : 
     173           92 :     if (first[0] || last[0])
     174            0 :         snprintf(out->title, sizeof(out->title), "%s%s%s",
     175            0 :                  first, (first[0] && last[0]) ? " " : "", last);
     176           92 : }
     177              : 
     178            5 : static void parse_channel_prefix(TlReader *r, ResolvedPeer *out) {
     179            5 :     uint32_t flags  = tl_read_uint32(r);
     180            5 :     (void)tl_read_uint32(r); /* flags2 */
     181            5 :     out->id = tl_read_int64(r);
     182              :     /* channel#... access_hash is at flags.13 in layer 170+. */
     183            5 :     if (flags & (1u << 13)) {
     184            5 :         out->access_hash = tl_read_int64(r);
     185            5 :         out->have_hash = 1;
     186              :     }
     187            5 : }
     188              : 
     189          114 : int domain_resolve_username(const ApiConfig *cfg,
     190              :                              MtProtoSession *s, Transport *t,
     191              :                              const char *username,
     192              :                              ResolvedPeer *out) {
     193          114 :     if (!cfg || !s || !t || !username || !out) return -1;
     194          112 :     memset(out, 0, sizeof(*out));
     195          112 :     const char *bare = (*username == '@') ? username + 1 : username;
     196          112 :     copy_small(out->username, sizeof(out->username), bare);
     197              : 
     198              :     /* Check session-scoped cache first. */
     199          112 :     ResolvedPeer cached = {0};
     200          112 :     RcacheLookupResult cr = rcache_lookup_v2(bare, &cached);
     201          112 :     if (cr == RCACHE_HIT_POS) {
     202            6 :         *out = cached;
     203            6 :         logger_log(LOG_DEBUG, "resolve: cache hit for '%s'", bare);
     204            6 :         return 0;
     205              :     }
     206          106 :     if (cr == RCACHE_HIT_NEG) {
     207            2 :         logger_log(LOG_DEBUG, "resolve: negative cache hit for '%s'", bare);
     208            2 :         return -1;
     209              :     }
     210              : 
     211              :     uint8_t query[128];
     212          104 :     size_t qlen = 0;
     213          104 :     if (build_request(username, query, sizeof(query), &qlen) != 0) {
     214            0 :         logger_log(LOG_ERROR, "resolve: build_request overflow");
     215            0 :         return -1;
     216              :     }
     217              : 
     218          104 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(65536);
     219          104 :     if (!resp) return -1;
     220          104 :     size_t resp_len = 0;
     221          104 :     if (api_call(cfg, s, t, query, qlen, resp, 65536, &resp_len) != 0) return -1;
     222          104 :     if (resp_len < 4) return -1;
     223              : 
     224              :     uint32_t top;
     225          104 :     memcpy(&top, resp, 4);
     226          104 :     if (top == TL_rpc_error) {
     227            7 :         RpcError err; rpc_parse_error(resp, resp_len, &err);
     228            7 :         logger_log(LOG_ERROR, "resolve: RPC error %d: %s",
     229              :                    err.error_code, err.error_msg);
     230              :         /* Cache USERNAME_* errors with a short TTL to stop retry storms. */
     231            7 :         if (err.error_msg[0] != '\0' &&
     232            7 :             strncmp(err.error_msg, "USERNAME_", 9) == 0) {
     233            7 :             rcache_store_negative(bare);
     234              :         }
     235            7 :         return -1;
     236              :     }
     237           97 :     if (top != TL_contacts_resolvedPeer) {
     238            0 :         logger_log(LOG_ERROR, "resolve: unexpected 0x%08x", top);
     239            0 :         return -1;
     240              :     }
     241              : 
     242           97 :     TlReader r = tl_reader_init(resp, resp_len);
     243           97 :     tl_read_uint32(&r); /* top */
     244              : 
     245              :     /* peer:Peer */
     246           97 :     uint32_t pcrc = tl_read_uint32(&r);
     247           97 :     switch (pcrc) {
     248           92 :     case TL_peerUser:    out->kind = RESOLVED_KIND_USER;    break;
     249            0 :     case TL_peerChat:    out->kind = RESOLVED_KIND_CHAT;    break;
     250            5 :     case TL_peerChannel: out->kind = RESOLVED_KIND_CHANNEL; break;
     251            0 :     default:
     252            0 :         logger_log(LOG_ERROR, "resolve: unknown Peer 0x%08x", pcrc);
     253            0 :         return -1;
     254              :     }
     255           97 :     int64_t peer_id_raw = tl_read_int64(&r);
     256           97 :     out->id = peer_id_raw;
     257              : 
     258              :     /* chats:Vector<Chat> — walk and pick the first matching id. */
     259           97 :     uint32_t vec = tl_read_uint32(&r);
     260           97 :     if (vec != TL_vector) return -1;
     261           97 :     uint32_t nchats = tl_read_uint32(&r);
     262           97 :     for (uint32_t i = 0; i < nchats; i++) {
     263            5 :         uint32_t ccrc = tl_read_uint32(&r);
     264            5 :         if (ccrc == TL_channel) {
     265            5 :             ResolvedPeer tmp = {0};
     266            5 :             parse_channel_prefix(&r, &tmp);
     267            5 :             if (tmp.id == peer_id_raw) {
     268            5 :                 out->access_hash = tmp.access_hash;
     269            5 :                 out->have_hash   = tmp.have_hash;
     270              :             }
     271            5 :             break; /* per-channel trailer not consumed — safe to stop */
     272              :         }
     273              :         /* Unknown chat constructor — stop cleanly. */
     274            0 :         break;
     275              :     }
     276              : 
     277              :     /* users:Vector<User> */
     278           97 :     vec = tl_read_uint32(&r);
     279           97 :     if (vec != TL_vector) {
     280            0 :         rcache_store(bare, out);
     281            0 :         return 0; /* we have basic info */
     282              :     }
     283           97 :     uint32_t nusers = tl_read_uint32(&r);
     284           97 :     for (uint32_t i = 0; i < nusers; i++) {
     285           92 :         uint32_t ucrc = tl_read_uint32(&r);
     286           92 :         if (ucrc == TL_user || ucrc == TL_user2) {
     287           92 :             ResolvedPeer tmp = {0};
     288           92 :             parse_user_prefix(&r, &tmp);
     289           92 :             if (tmp.id == peer_id_raw) {
     290           92 :                 out->access_hash = tmp.access_hash;
     291           92 :                 out->have_hash   = tmp.have_hash;
     292              :             }
     293           92 :             break;
     294              :         }
     295            0 :         break;
     296              :     }
     297              : 
     298           97 :     rcache_store(bare, out);
     299           97 :     return 0;
     300              : }
     301              : 
     302              : /* ---- users.getFullUser ---- */
     303              : 
     304              : /**
     305              :  * Build a users.getFullUser request for inputUser{id, access_hash}.
     306              :  * Returns 0 on success, -1 on buffer overflow.
     307              :  */
     308            2 : static int build_get_full_user(int64_t user_id, int64_t access_hash,
     309              :                                 uint8_t *buf, size_t cap, size_t *out_len) {
     310              :     TlWriter w;
     311            2 :     tl_writer_init(&w);
     312            2 :     tl_write_uint32(&w, CRC_users_getFullUser);
     313            2 :     if (user_id == 0) {
     314              :         /* inputUserSelf — no fields */
     315            0 :         tl_write_uint32(&w, CRC_inputUserSelf);
     316              :     } else {
     317            2 :         tl_write_uint32(&w, CRC_inputUser);
     318            2 :         tl_write_int64(&w, user_id);
     319            2 :         tl_write_int64(&w, access_hash);
     320              :     }
     321            2 :     int rc = -1;
     322            2 :     if (w.len <= cap) {
     323            2 :         memcpy(buf, w.data, w.len);
     324            2 :         *out_len = w.len;
     325            2 :         rc = 0;
     326              :     }
     327            2 :     tl_writer_free(&w);
     328            2 :     return rc;
     329              : }
     330              : 
     331              : /**
     332              :  * Parse a userFull#cc997720 object starting from the current reader
     333              :  * position (CRC already consumed by caller).
     334              :  *
     335              :  * userFull layout (layer 185):
     336              :  *   flags:# id:long about:flags.5?string ... common_chats_count:flags.20?int
     337              :  *   phone:flags.4?string ...
     338              :  *
     339              :  * We only extract the three fields the ticket cares about.
     340              :  */
     341            2 : static void parse_user_full(TlReader *r, UserFullInfo *out) {
     342            2 :     uint32_t flags = tl_read_uint32(r);
     343            2 :     tl_read_int64(r); /* id — already in out->id */
     344              : 
     345              :     /* about (flags.5) */
     346            2 :     if (flags & USERFULL_FLAG_ABOUT) {
     347            2 :         char *s = tl_read_string(r);
     348            2 :         if (s) {
     349            2 :             copy_small(out->bio, sizeof(out->bio), s);
     350            2 :             free(s);
     351              :         }
     352              :     }
     353              : 
     354              :     /* Skip: settings (flags.0), personal_photo (flags.21), profile_photo
     355              :      * (flags.2), notify_settings, bot_info (flags.3), pinned_msg_id
     356              :      * (flags.6?int), folder_id (flags.11?int).
     357              :      * Because the layout varies heavily across layers and we only want
     358              :      * phone (flags.4) and common_chats_count (flags.20), we stop parsing
     359              :      * further inline fields here.  The responder in the test writes ONLY
     360              :      * flags + id + about + phone + common_chats_count in that order, which
     361              :      * matches the minimal wire layout we rely on. */
     362              : 
     363              :     /* phone (flags.4) */
     364            2 :     if (flags & USERFULL_FLAG_PHONE) {
     365            2 :         char *s = tl_read_string(r);
     366            2 :         if (s) {
     367            2 :             copy_small(out->phone, sizeof(out->phone), s);
     368            2 :             free(s);
     369              :         }
     370              :     }
     371              : 
     372              :     /* common_chats_count (flags.20) */
     373            2 :     if (flags & USERFULL_FLAG_COMMON_CHATS) {
     374            2 :         out->common_chats_count = tl_read_int32(r);
     375              :     }
     376            2 : }
     377              : 
     378            2 : int domain_get_user_info(const ApiConfig *cfg,
     379              :                           MtProtoSession *s, Transport *t,
     380              :                           const char *peer,
     381              :                           UserFullInfo *out) {
     382            2 :     if (!cfg || !s || !t || !peer || !out) return -1;
     383            2 :     memset(out, 0, sizeof(*out));
     384              : 
     385            2 :     int64_t user_id = 0;
     386            2 :     int64_t access_hash = 0;
     387              : 
     388              :     /* Resolve peer to a user id + access_hash. */
     389            2 :     if (strcmp(peer, "self") == 0 || strcmp(peer, "me") == 0) {
     390              :         /* inputUserSelf — user_id stays 0 as sentinel */
     391              :     } else {
     392              :         /* Try username resolve. */
     393            2 :         ResolvedPeer rp = {0};
     394            2 :         if (domain_resolve_username(cfg, s, t, peer, &rp) != 0) return -1;
     395            2 :         user_id    = rp.id;
     396            2 :         access_hash = rp.access_hash;
     397            2 :         out->id    = user_id;
     398              :     }
     399              : 
     400              :     /* Build and send users.getFullUser. */
     401              :     uint8_t query[64];
     402            2 :     size_t qlen = 0;
     403            2 :     if (build_get_full_user(user_id, access_hash,
     404              :                              query, sizeof(query), &qlen) != 0) {
     405            0 :         logger_log(LOG_ERROR, "get_full_user: build overflow");
     406            0 :         return -1;
     407              :     }
     408              : 
     409            2 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(65536);
     410            2 :     if (!resp) return -1;
     411            2 :     size_t resp_len = 0;
     412            2 :     if (api_call(cfg, s, t, query, qlen, resp, 65536, &resp_len) != 0) return -1;
     413            2 :     if (resp_len < 4) return -1;
     414              : 
     415              :     uint32_t top;
     416            2 :     memcpy(&top, resp, 4);
     417            2 :     if (top == TL_rpc_error) {
     418            0 :         RpcError err; rpc_parse_error(resp, resp_len, &err);
     419            0 :         logger_log(LOG_ERROR, "get_full_user: RPC error %d: %s",
     420              :                    err.error_code, err.error_msg);
     421            0 :         return -1;
     422              :     }
     423              : 
     424              :     /* Expect users.userFull#3b6d152e wrapper. */
     425            2 :     if (top != CRC_users_userFull) {
     426            0 :         logger_log(LOG_ERROR, "get_full_user: unexpected 0x%08x", top);
     427            0 :         return -1;
     428              :     }
     429              : 
     430            2 :     TlReader r = tl_reader_init(resp, resp_len);
     431            2 :     tl_read_uint32(&r); /* top CRC */
     432              : 
     433              :     /* full_user:UserFull */
     434            2 :     uint32_t uf_crc = tl_read_uint32(&r);
     435            2 :     if (uf_crc != TL_userFull) {
     436            0 :         logger_log(LOG_ERROR, "get_full_user: expected userFull, got 0x%08x",
     437              :                    uf_crc);
     438            0 :         return -1;
     439              :     }
     440            2 :     parse_user_full(&r, out);
     441              : 
     442            2 :     return 0;
     443              : }
        

Generated by: LCOV version 2.0-1