LCOV - code coverage report
Current view: top level - src/domain/read - history.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 87.9 % 354 311
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 10 10

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file domain/read/history.c
       6              :  * @brief Minimal messages.getHistory parser (US-06 v1).
       7              :  */
       8              : 
       9              : #include "domain/read/history.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 <stdlib.h>
      19              : #include <string.h>
      20              : 
      21              : #define CRC_messages_getHistory 0x4423e6c5u
      22              : #define CRC_inputPeerSelf_local TL_inputPeerSelf /* alias for readability */
      23              : 
      24              : /* ---- MessageAction CRCs (US-29) ----
      25              :  * Stable across recent TL layers (170+). See docs/userstory/US-29 for
      26              :  * the rendering policy. Unknown action CRCs fall through to a labelled
      27              :  * "[service action 0x%08x]" string to keep group histories dense even
      28              :  * when Telegram adds new variants. */
      29              : #define CRC_messageActionEmpty                0xb6aef7b0u
      30              : #define CRC_messageActionChatCreate           0xbd47cbadu
      31              : #define CRC_messageActionChatEditTitle        0xb5a1ce5au
      32              : #define CRC_messageActionChatEditPhoto        0x7fcb13a8u
      33              : #define CRC_messageActionChatDeletePhoto      0x95e3fbefu
      34              : #define CRC_messageActionChatAddUser          0x15cefd00u
      35              : #define CRC_messageActionChatDeleteUser       0xa43f30ccu
      36              : #define CRC_messageActionChatJoinedByLink     0x031224c3u
      37              : #define CRC_messageActionChannelCreate        0x95d2ac92u
      38              : #define CRC_messageActionChatMigrateTo        0xe1037f92u
      39              : #define CRC_messageActionChannelMigrateFrom   0xea3948e9u
      40              : #define CRC_messageActionPinMessage           0x94bd38edu
      41              : #define CRC_messageActionHistoryClear         0x9fbab604u
      42              : #define CRC_messageActionPhoneCall            0x80e11a7fu
      43              : #define CRC_messageActionScreenshotTaken      0x4792929bu
      44              : #define CRC_messageActionCustomAction         0xfae69f56u
      45              : #define CRC_messageActionGroupCall            0x7a0d7f42u
      46              : #define CRC_messageActionGroupCallScheduled   0xb3a07661u
      47              : #define CRC_messageActionInviteToGroupCall    0x502f92f7u
      48              : 
      49              : /* Phone-call discard reason CRCs (flags.0 on PhoneCall). */
      50              : #define CRC_phoneCallDiscardReasonMissed      0x85e42301u
      51              : #define CRC_phoneCallDiscardReasonDisconnect  0xe095c1a0u
      52              : #define CRC_phoneCallDiscardReasonHangup      0x57adc690u
      53              : #define CRC_phoneCallDiscardReasonBusy        0xfaf7e8c9u
      54              : 
      55          142 : static int write_input_peer(TlWriter *w, const HistoryPeer *p) {
      56          142 :     switch (p->kind) {
      57          125 :     case HISTORY_PEER_SELF:
      58          125 :         tl_write_uint32(w, TL_inputPeerSelf);
      59          125 :         return 0;
      60            8 :     case HISTORY_PEER_USER:
      61            8 :         tl_write_uint32(w, TL_inputPeerUser);
      62            8 :         tl_write_int64 (w, p->peer_id);
      63            8 :         tl_write_int64 (w, p->access_hash);
      64            8 :         return 0;
      65            0 :     case HISTORY_PEER_CHAT:
      66            0 :         tl_write_uint32(w, TL_inputPeerChat);
      67            0 :         tl_write_int64 (w, p->peer_id);
      68            0 :         return 0;
      69            9 :     case HISTORY_PEER_CHANNEL:
      70            9 :         tl_write_uint32(w, TL_inputPeerChannel);
      71            9 :         tl_write_int64 (w, p->peer_id);
      72            9 :         tl_write_int64 (w, p->access_hash);
      73            9 :         return 0;
      74            0 :     default:
      75            0 :         return -1;
      76              :     }
      77              : }
      78              : 
      79              : /* Field bit positions used to decide whether to skip the first-stage
      80              :  * Message prefix (before we abort and move on to the next message).
      81              :  * These correspond to layer 170+ but are stable across recent layers. */
      82              : #define MSG_FLAG_OUT              (1u << 1)
      83              : #define MSG_FLAG_HAS_FROM_ID      (1u << 8)
      84              : 
      85              : /* All trailer flags now have skippers; only factcheck (flags2.3)
      86              :  * still halts iteration. */
      87              : 
      88          142 : static int build_request(const HistoryPeer *peer, int32_t offset_id, int limit,
      89              :                           uint8_t *buf, size_t cap, size_t *out_len) {
      90              :     TlWriter w;
      91          142 :     tl_writer_init(&w);
      92          142 :     tl_write_uint32(&w, CRC_messages_getHistory);
      93          142 :     if (write_input_peer(&w, peer) != 0) {
      94            0 :         tl_writer_free(&w);
      95            0 :         return -1;
      96              :     }
      97          142 :     tl_write_int32 (&w, offset_id);
      98          142 :     tl_write_int32 (&w, 0);  /* offset_date */
      99          142 :     tl_write_int32 (&w, 0);  /* add_offset */
     100          142 :     tl_write_int32 (&w, limit);
     101          142 :     tl_write_int32 (&w, 0);  /* max_id */
     102          142 :     tl_write_int32 (&w, 0);  /* min_id */
     103          142 :     tl_write_int64 (&w, 0);  /* hash */
     104              : 
     105          142 :     int rc = -1;
     106          142 :     if (w.len <= cap) {
     107          142 :         memcpy(buf, w.data, w.len);
     108          142 :         *out_len = w.len;
     109          142 :         rc = 0;
     110              :     }
     111          142 :     tl_writer_free(&w);
     112          142 :     return rc;
     113              : }
     114              : 
     115              : /* Copy src into out->text with HISTORY_TEXT_MAX truncation. */
     116           44 : static void set_text(HistoryEntry *out, const char *src) {
     117           44 :     if (!src) { out->text[0] = '\0'; return; }
     118           44 :     size_t n = strlen(src);
     119           44 :     if (n >= HISTORY_TEXT_MAX) { n = HISTORY_TEXT_MAX - 1; out->truncated = 1; }
     120           44 :     memcpy(out->text, src, n);
     121           44 :     out->text[n] = '\0';
     122              : }
     123              : 
     124              : /* Read and discard a Vector<long>. Returns 0 on success. */
     125            4 : static int skip_long_vector(TlReader *r) {
     126            4 :     if (r->len - r->pos < 8) return -1;
     127            4 :     if (tl_read_uint32(r) != TL_vector) return -1;
     128            4 :     uint32_t n = tl_read_uint32(r);
     129            4 :     if (n > 4096) return -1; /* sanity cap */
     130            4 :     if (r->len - r->pos < (size_t)n * 8) return -1;
     131           10 :     for (uint32_t i = 0; i < n; i++) tl_read_int64(r);
     132            4 :     return 0;
     133              : }
     134              : 
     135              : /* Discard the remaining Vector<long>; return the first element (or 0). */
     136            2 : static int read_long_vector_first(TlReader *r, int64_t *first) {
     137            2 :     if (r->len - r->pos < 8) return -1;
     138            2 :     if (tl_read_uint32(r) != TL_vector) return -1;
     139            2 :     uint32_t n = tl_read_uint32(r);
     140            2 :     if (n > 4096) return -1;
     141            2 :     if (r->len - r->pos < (size_t)n * 8) return -1;
     142            2 :     if (first) *first = 0;
     143            4 :     for (uint32_t i = 0; i < n; i++) {
     144            2 :         int64_t v = tl_read_int64(r);
     145            2 :         if (i == 0 && first) *first = v;
     146              :     }
     147            2 :     return 0;
     148              : }
     149              : 
     150              : /* Read a MessageAction and render a human-readable string into out->text.
     151              :  * Sets out->is_service=1 in all cases. Returns 0 if the action was fully
     152              :  * consumed (cursor past), -1 on truncation / unknown substructure. */
     153           44 : static int parse_service_action(TlReader *r, HistoryEntry *out) {
     154           44 :     out->is_service = 1;
     155           44 :     if (r->len - r->pos < 4) { set_text(out, "[service action truncated]"); return -1; }
     156           44 :     uint32_t acrc = tl_read_uint32(r);
     157              :     char buf[HISTORY_TEXT_MAX];
     158              : 
     159           44 :     switch (acrc) {
     160            2 :     case CRC_messageActionEmpty:
     161            2 :         set_text(out, "");
     162            2 :         return 0;
     163            2 :     case CRC_messageActionChatCreate: {
     164              :         /* title:string users:Vector<long> */
     165            4 :         RAII_STRING char *title = tl_read_string(r);
     166            2 :         if (!title) return -1;
     167            2 :         if (skip_long_vector(r) != 0) return -1;
     168            2 :         snprintf(buf, sizeof(buf), "created group '%s'", title);
     169            2 :         set_text(out, buf);
     170            2 :         return 0;
     171              :     }
     172            2 :     case CRC_messageActionChatEditTitle: {
     173              :         /* title:string */
     174            4 :         RAII_STRING char *title = tl_read_string(r);
     175            2 :         if (!title) return -1;
     176            2 :         snprintf(buf, sizeof(buf), "changed title to '%s'", title);
     177            2 :         set_text(out, buf);
     178            2 :         return 0;
     179              :     }
     180            2 :     case CRC_messageActionChatEditPhoto: {
     181              :         /* photo:Photo — skip via tl_skip_photo */
     182            2 :         if (tl_skip_photo(r) != 0) { set_text(out, "changed group photo"); return -1; }
     183            2 :         set_text(out, "changed group photo");
     184            2 :         return 0;
     185              :     }
     186            2 :     case CRC_messageActionChatDeletePhoto:
     187            2 :         set_text(out, "removed group photo");
     188            2 :         return 0;
     189            2 :     case CRC_messageActionChatAddUser: {
     190              :         /* users:Vector<long> */
     191            2 :         int64_t first = 0;
     192            2 :         if (read_long_vector_first(r, &first) != 0) return -1;
     193            2 :         snprintf(buf, sizeof(buf), "added @%lld", (long long)first);
     194            2 :         set_text(out, buf);
     195            2 :         return 0;
     196              :     }
     197            2 :     case CRC_messageActionChatDeleteUser: {
     198              :         /* user_id:long */
     199            2 :         if (r->len - r->pos < 8) return -1;
     200            2 :         int64_t uid = tl_read_int64(r);
     201            2 :         snprintf(buf, sizeof(buf), "removed @%lld", (long long)uid);
     202            2 :         set_text(out, buf);
     203            2 :         return 0;
     204              :     }
     205            2 :     case CRC_messageActionChatJoinedByLink: {
     206              :         /* inviter_id:long */
     207            2 :         if (r->len - r->pos < 8) return -1;
     208            2 :         (void)tl_read_int64(r);
     209            2 :         set_text(out, "joined via invite link");
     210            2 :         return 0;
     211              :     }
     212            2 :     case CRC_messageActionChannelCreate: {
     213              :         /* title:string */
     214            4 :         RAII_STRING char *title = tl_read_string(r);
     215            2 :         if (!title) return -1;
     216            2 :         snprintf(buf, sizeof(buf), "created channel '%s'", title);
     217            2 :         set_text(out, buf);
     218            2 :         return 0;
     219              :     }
     220            2 :     case CRC_messageActionChatMigrateTo: {
     221              :         /* channel_id:long */
     222            2 :         if (r->len - r->pos < 8) return -1;
     223            2 :         int64_t cid = tl_read_int64(r);
     224            2 :         snprintf(buf, sizeof(buf), "migrated to channel %lld", (long long)cid);
     225            2 :         set_text(out, buf);
     226            2 :         return 0;
     227              :     }
     228            2 :     case CRC_messageActionChannelMigrateFrom: {
     229              :         /* title:string chat_id:long */
     230            4 :         RAII_STRING char *title = tl_read_string(r);
     231            2 :         if (!title) return -1;
     232            2 :         if (r->len - r->pos < 8) return -1;
     233            2 :         int64_t cid = tl_read_int64(r);
     234            2 :         snprintf(buf, sizeof(buf), "migrated from group %lld '%s'",
     235              :                  (long long)cid, title);
     236            2 :         set_text(out, buf);
     237            2 :         return 0;
     238              :     }
     239            4 :     case CRC_messageActionPinMessage:
     240              :         /* Pinned message id is carried on the outer reply_to header, not
     241              :          * on the action itself. The outer parser captured it already. */
     242            4 :         if (out->text[0] == '\0') set_text(out, "pinned message");
     243            4 :         return 0;
     244            2 :     case CRC_messageActionHistoryClear:
     245            2 :         set_text(out, "history cleared");
     246            2 :         return 0;
     247            4 :     case CRC_messageActionPhoneCall: {
     248              :         /* flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int */
     249            4 :         if (r->len - r->pos < 4 + 8) return -1;
     250            4 :         uint32_t pflags = tl_read_uint32(r);
     251            4 :         (void)tl_read_int64(r); /* call_id */
     252            4 :         const char *reason = "completed";
     253            4 :         if (pflags & 1u) {
     254            4 :             if (r->len - r->pos < 4) return -1;
     255            4 :             uint32_t rcrc = tl_read_uint32(r);
     256            4 :             switch (rcrc) {
     257            2 :             case CRC_phoneCallDiscardReasonMissed:     reason = "missed"; break;
     258            0 :             case CRC_phoneCallDiscardReasonDisconnect: reason = "disconnected"; break;
     259            2 :             case CRC_phoneCallDiscardReasonHangup:     reason = "hangup"; break;
     260            0 :             case CRC_phoneCallDiscardReasonBusy:       reason = "busy"; break;
     261            0 :             default: break;
     262              :             }
     263              :         }
     264            4 :         int duration = 0;
     265            4 :         if (pflags & (1u << 1)) {
     266            2 :             if (r->len - r->pos < 4) return -1;
     267            2 :             duration = tl_read_int32(r);
     268              :         }
     269            4 :         snprintf(buf, sizeof(buf), "called (duration %ds, %s)", duration, reason);
     270            4 :         set_text(out, buf);
     271            4 :         return 0;
     272              :     }
     273            2 :     case CRC_messageActionScreenshotTaken:
     274            2 :         set_text(out, "took screenshot");
     275            2 :         return 0;
     276            2 :     case CRC_messageActionCustomAction: {
     277              :         /* message:string — the raw action label chosen by the server. */
     278            4 :         RAII_STRING char *msg = tl_read_string(r);
     279            2 :         if (!msg) return -1;
     280            2 :         set_text(out, msg);
     281            2 :         return 0;
     282              :     }
     283            2 :     case CRC_messageActionGroupCall: {
     284              :         /* flags:# call:InputGroupCall duration:flags.0?int
     285              :          * inputGroupCall#d8aa840f id:long access_hash:long */
     286            2 :         if (r->len - r->pos < 4) return -1;
     287            2 :         uint32_t gflags = tl_read_uint32(r);
     288            2 :         if (r->len - r->pos < 4 + 16) return -1;
     289            2 :         tl_read_uint32(r); /* inputGroupCall crc */
     290            2 :         tl_read_int64(r);  /* id */
     291            2 :         tl_read_int64(r);  /* access_hash */
     292            2 :         if (gflags & 1u) {
     293            0 :             if (r->len - r->pos < 4) return -1;
     294            0 :             tl_read_int32(r); /* duration */
     295              :         }
     296            2 :         set_text(out, "started video chat");
     297            2 :         return 0;
     298              :     }
     299            2 :     case CRC_messageActionGroupCallScheduled: {
     300              :         /* call:InputGroupCall schedule_date:int */
     301            2 :         if (r->len - r->pos < 4 + 16 + 4) return -1;
     302            2 :         tl_read_uint32(r); tl_read_int64(r); tl_read_int64(r);
     303            2 :         int sched = tl_read_int32(r);
     304            2 :         snprintf(buf, sizeof(buf), "scheduled video chat for %d", sched);
     305            2 :         set_text(out, buf);
     306            2 :         return 0;
     307              :     }
     308            2 :     case CRC_messageActionInviteToGroupCall: {
     309              :         /* call:InputGroupCall users:Vector<long> */
     310            2 :         if (r->len - r->pos < 4 + 16) return -1;
     311            2 :         tl_read_uint32(r); tl_read_int64(r); tl_read_int64(r);
     312            2 :         if (skip_long_vector(r) != 0) return -1;
     313            2 :         set_text(out, "invited to video chat");
     314            2 :         return 0;
     315              :     }
     316            2 :     default:
     317            2 :         snprintf(buf, sizeof(buf), "[service action 0x%08x]", acrc);
     318            2 :         set_text(out, buf);
     319              :         /* Unknown trailing data: we cannot advance reliably, but the
     320              :          * enclosing vector iterator will stop on rc=-1 from parse_message.
     321              :          * Return 0 so the entry is emitted with its label. */
     322            2 :         return 0;
     323              :     }
     324              : }
     325              : 
     326              : /* Parse the messageService layout and render the action into out->text.
     327              :  * Cursor is positioned just after (flags, id) on entry — messageService
     328              :  * does not carry flags2 on the current TL schema. Shared with updates.c
     329              :  * and search.c via `domain_history_parse_service` (see header). */
     330           44 : int domain_history_parse_service(TlReader *r, HistoryEntry *out, uint32_t flags) {
     331           44 :     out->is_service = 1;
     332           44 :     if (flags & MSG_FLAG_HAS_FROM_ID) {
     333            0 :         if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     334              :     }
     335           44 :     if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     336           44 :     if (flags & (1u << 3)) { /* reply_to — for pinMessage the target id lives here. */
     337              :         /* Peek the first 3 words (crc + flags + first optional) without
     338              :          * advancing: messageReplyHeader#afbc09db always has flags bit 4 =
     339              :          * reply_to_msg_id as the first int32 payload when present. */
     340            4 :         if (r->len - r->pos >= 12) {
     341              :             uint32_t rh_crc, rh_flags;
     342            4 :             memcpy(&rh_crc,   r->data + r->pos,     4);
     343            4 :             memcpy(&rh_flags, r->data + r->pos + 4, 4);
     344            4 :             if (rh_crc == 0xafbc09dbu && (rh_flags & (1u << 4))) {
     345              :                 int32_t pinned;
     346            4 :                 memcpy(&pinned, r->data + r->pos + 8, 4);
     347              :                 char buf[HISTORY_TEXT_MAX];
     348            4 :                 snprintf(buf, sizeof(buf), "pinned message %d", pinned);
     349            4 :                 set_text(out, buf);
     350              :             }
     351              :         }
     352            4 :         if (tl_skip_message_reply_header(r) != 0) { out->complex = 1; return -1; }
     353              :     }
     354           44 :     if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     355           44 :     out->date = (int64_t)(int32_t)tl_read_int32(r);
     356              :     /* Preserve any pre-populated text (e.g. "pinned message N") across
     357              :      * parse_service_action for the PinMessage case, which has no body. */
     358              :     char saved_text[HISTORY_TEXT_MAX];
     359           44 :     memcpy(saved_text, out->text, sizeof(saved_text));
     360           44 :     int rc = parse_service_action(r, out);
     361           44 :     if (out->text[0] == '\0' && saved_text[0] != '\0')
     362            0 :         memcpy(out->text, saved_text, sizeof(saved_text));
     363              :     /* ttl_period (flags.25) — optional. Best-effort skip. */
     364           44 :     if (rc == 0 && (flags & (1u << 25))) {
     365            0 :         if (r->len - r->pos >= 4) tl_read_int32(r);
     366              :     }
     367           44 :     return rc;
     368              : }
     369              : 
     370              : /* Parse a Message. On success advance r past the whole object so the
     371              :  * caller can read the next vector element. Returns 0 on success, -1 on
     372              :  * parse failure, and sets `out->complex = 1` when we got the text/date
     373              :  * but had to bail before reaching the end of the object. */
     374         2108 : static int parse_message(TlReader *r, HistoryEntry *out) {
     375         2108 :     if (!tl_reader_ok(r)) return -1;
     376         2108 :     uint32_t crc = tl_read_uint32(r);
     377              : 
     378         2108 :     if (crc == TL_messageEmpty) {
     379            4 :         uint32_t flags = tl_read_uint32(r);
     380            4 :         out->id = tl_read_int32(r);
     381            4 :         if (flags & 1u) { tl_read_uint32(r); tl_read_int64(r); }
     382            4 :         return 0;
     383              :     }
     384              : 
     385         2104 :     if (crc != TL_message && crc != TL_messageService) {
     386            0 :         logger_log(LOG_WARN, "history: unknown Message 0x%08x", crc);
     387            0 :         return -1;
     388              :     }
     389              : 
     390         2104 :     uint32_t flags = tl_read_uint32(r);
     391         2104 :     uint32_t flags2 = 0;
     392              :     /* TL_message carries flags2; TL_messageService does not on the upstream
     393              :      * schema. Tests and the in-process mock server mirror this layout. */
     394         2104 :     if (crc == TL_message) flags2 = tl_read_uint32(r);
     395         2104 :     out->out = (flags & MSG_FLAG_OUT) ? 1 : 0;
     396         2104 :     out->id  = tl_read_int32(r);
     397              : 
     398         2104 :     if (crc == TL_messageService) {
     399              :         /* US-29 — render the action into out->text and return it as a
     400              :          * normal row. The enclosing vector iterator continues. */
     401           42 :         return domain_history_parse_service(r, out, flags);
     402              :     }
     403              : 
     404              :     /* Pre-text fields (message is read AFTER these). */
     405         2062 :     if (flags & MSG_FLAG_HAS_FROM_ID) {
     406            0 :         if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     407              :     }
     408         2062 :     if (tl_skip_peer(r) != 0)         { out->complex = 1; return -1; }
     409         2061 :     if (flags & (1u << 28)) { /* saved_peer_id (layer 185+) */
     410            2 :         if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     411              :     }
     412         2061 :     if (flags & (1u << 2)) { /* fwd_from */
     413            4 :         if (tl_skip_message_fwd_header(r) != 0) { out->complex = 1; return -1; }
     414              :     }
     415         2061 :     if (flags & (1u << 11)) { /* via_bot_id */
     416            6 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     417            6 :         tl_read_int64(r);
     418              :     }
     419         2061 :     if (flags2 & (1u << 0)) { /* via_business_bot_id */
     420            2 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     421            2 :         tl_read_int64(r);
     422              :     }
     423         2061 :     if (flags & (1u << 3)) { /* reply_to */
     424            4 :         if (tl_skip_message_reply_header(r) != 0) { out->complex = 1; return -1; }
     425              :     }
     426         2061 :     if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     427         2061 :     out->date = (int64_t)(int32_t)tl_read_int32(r);
     428              : 
     429              :     /* message:string — always present. */
     430         4122 :     RAII_STRING char *msg = tl_read_string(r);
     431         2061 :     if (msg) {
     432         2061 :         size_t n = strlen(msg);
     433         2061 :         if (n >= HISTORY_TEXT_MAX) { n = HISTORY_TEXT_MAX - 1; out->truncated = 1; }
     434         2061 :         memcpy(out->text, msg, n);
     435         2061 :         out->text[n] = '\0';
     436              :     }
     437              : 
     438              :     /* Skippable optionals after `message` — in schema order:
     439              :      *   flags.9  media
     440              :      *   flags.6  reply_markup (BAIL — no skipper yet)
     441              :      *   flags.7  entities
     442              :      *   flags.10 views + forwards
     443              :      *   ... (more scalars below)
     444              :      *
     445              :      * Media is attempted first; if it fails the Message is left
     446              :      * mid-parse but we have at least captured id/date/text. */
     447         2061 :     if (flags & (1u << 9)) {
     448           27 :         MediaInfo mi = {0};
     449           27 :         if (tl_skip_message_media_ex(r, &mi) != 0) {
     450            0 :             out->media = mi.kind;
     451            0 :             out->complex = 1;
     452            0 :             return -1;
     453              :         }
     454           27 :         out->media    = mi.kind;
     455           54 :         out->media_id = (mi.kind == MEDIA_PHOTO) ? mi.photo_id
     456           27 :                       : (mi.kind == MEDIA_DOCUMENT) ? mi.document_id : 0;
     457           27 :         out->media_dc = mi.dc_id;
     458           27 :         out->media_info = mi;
     459              :     }
     460              : 
     461              :     /* Per-layer order: media → reply_markup → entities → views/forwards
     462              :      * → replies → edit_date → post_author → grouped_id → reactions →
     463              :      * restriction_reason → ttl_period → ... replies + restriction_reason
     464              :      * still don't have skippers. */
     465         2061 :     if (flags & (1u << 6)) { /* reply_markup */
     466            2 :         if (tl_skip_reply_markup(r) != 0) { out->complex = 1; return -1; }
     467              :     }
     468         2060 :     if (flags & (1u << 7))  { /* entities */
     469            1 :         if (tl_skip_message_entities_vector(r) != 0) { out->complex = 1; return -1; }
     470              :     }
     471         2060 :     if (flags & (1u << 10)) { /* views + forwards */
     472            0 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     473            0 :         tl_read_int32(r); tl_read_int32(r);
     474              :     }
     475         2060 :     if (flags & (1u << 23)) { /* replies */
     476            1 :         if (tl_skip_message_replies(r) != 0) { out->complex = 1; return -1; }
     477              :     }
     478         2060 :     if (flags & (1u << 15)) { /* edit_date */
     479            0 :         if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     480            0 :         tl_read_int32(r);
     481              :     }
     482         2060 :     if (flags & (1u << 16)) { /* post_author */
     483            0 :         if (tl_skip_string(r) != 0) { out->complex = 1; return -1; }
     484              :     }
     485         2060 :     if (flags & (1u << 17)) { /* grouped_id */
     486            0 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     487            0 :         tl_read_int64(r);
     488              :     }
     489         2060 :     if (flags & (1u << 20)) { /* reactions */
     490            1 :         if (tl_skip_message_reactions(r) != 0) { out->complex = 1; return -1; }
     491              :     }
     492         2060 :     if (flags & (1u << 22)) { /* restriction_reason */
     493            1 :         if (tl_skip_restriction_reason_vector(r) != 0) {
     494            0 :             out->complex = 1; return -1;
     495              :         }
     496              :     }
     497         2060 :     if (flags & (1u << 25)) { /* ttl_period */
     498            0 :         if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     499            0 :         tl_read_int32(r);
     500              :     }
     501         2060 :     if (flags2 & (1u << 30)) { /* quick_reply_shortcut_id */
     502            0 :         if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     503            0 :         tl_read_int32(r);
     504              :     }
     505         2060 :     if (flags2 & (1u << 2)) { /* effect */
     506            0 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     507            0 :         tl_read_int64(r);
     508              :     }
     509         2060 :     if (flags2 & (1u << 3)) { /* factcheck */
     510            0 :         if (tl_skip_factcheck(r) != 0) { out->complex = 1; return -1; }
     511              :     }
     512         2060 :     return 0;
     513              : }
     514              : 
     515          144 : int domain_get_history(const ApiConfig *cfg,
     516              :                         MtProtoSession *s, Transport *t,
     517              :                         const HistoryPeer *peer,
     518              :                         int32_t offset_id, int limit,
     519              :                         HistoryEntry *out, int *out_count) {
     520          144 :     if (!cfg || !s || !t || !peer || !out || !out_count || limit <= 0) return -1;
     521          142 :     *out_count = 0;
     522              : 
     523              :     uint8_t query[128];
     524          142 :     size_t qlen = 0;
     525          142 :     if (build_request(peer, offset_id, limit, query, sizeof(query), &qlen) != 0) {
     526            0 :         logger_log(LOG_ERROR, "history: build_request overflow");
     527            0 :         return -1;
     528              :     }
     529              : 
     530          142 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(262144);
     531          142 :     if (!resp) return -1;
     532          142 :     size_t resp_len = 0;
     533          142 :     if (api_call(cfg, s, t, query, qlen, resp, 262144, &resp_len) != 0) {
     534            2 :         logger_log(LOG_ERROR, "history: api_call failed");
     535            2 :         return -1;
     536              :     }
     537          140 :     if (resp_len < 8) return -1;
     538              : 
     539              :     uint32_t top;
     540          140 :     memcpy(&top, resp, 4);
     541          140 :     if (top == TL_rpc_error) {
     542              :         RpcError err;
     543            1 :         rpc_parse_error(resp, resp_len, &err);
     544            1 :         logger_log(LOG_ERROR, "history: RPC error %d: %s",
     545              :                    err.error_code, err.error_msg);
     546            1 :         return -1;
     547              :     }
     548              : 
     549          139 :     if (top != TL_messages_messages &&
     550           23 :         top != TL_messages_messagesSlice &&
     551            7 :         top != TL_messages_channelMessages) {
     552            0 :         logger_log(LOG_ERROR, "history: unexpected top 0x%08x", top);
     553            0 :         return -1;
     554              :     }
     555              : 
     556          139 :     TlReader r = tl_reader_init(resp, resp_len);
     557          139 :     tl_read_uint32(&r); /* top */
     558              : 
     559              :     /* messagesSlice/channelMessages prepend some counters we skip. */
     560          139 :     if (top == TL_messages_messagesSlice) {
     561           16 :         tl_read_uint32(&r); /* flags */
     562           16 :         tl_read_int32 (&r); /* count */
     563              :         /* next_rate + offset_id_offset are optional (flags.0/.2) — we
     564              :          * don't read them; the messages vector follows after them in the
     565              :          * wire. For robustness the parse stops after one entry anyway. */
     566          123 :     } else if (top == TL_messages_channelMessages) {
     567            7 :         tl_read_uint32(&r); /* flags */
     568            7 :         tl_read_int32 (&r); /* pts */
     569            7 :         tl_read_int32 (&r); /* count */
     570              :         /* optional offset_id_offset (flags.2) not read */
     571              :     }
     572              : 
     573          139 :     uint32_t vec = tl_read_uint32(&r);
     574          139 :     if (vec != TL_vector) {
     575            0 :         logger_log(LOG_ERROR, "history: expected Vector<Message>, got 0x%08x",
     576              :                    vec);
     577            0 :         return -1;
     578              :     }
     579          139 :     uint32_t count = tl_read_uint32(&r);
     580          139 :     int written = 0;
     581         2245 :     for (uint32_t i = 0; i < count && written < limit; i++) {
     582         2108 :         HistoryEntry e = {0};
     583         2108 :         int rc2 = parse_message(&r, &e);
     584         2108 :         if (e.id != 0 || e.text[0] != '\0') {
     585         2108 :             out[written++] = e;
     586              :         }
     587         2108 :         if (rc2 != 0) break; /* could not advance past message — stop */
     588              :     }
     589          139 :     *out_count = written;
     590          139 :     return 0;
     591              : }
     592              : 
     593          123 : int domain_get_history_self(const ApiConfig *cfg,
     594              :                              MtProtoSession *s, Transport *t,
     595              :                              int32_t offset_id, int limit,
     596              :                              HistoryEntry *out, int *out_count) {
     597          123 :     HistoryPeer self = { .kind = HISTORY_PEER_SELF };
     598          123 :     return domain_get_history(cfg, s, t, &self, offset_id, limit, out, out_count);
     599              : }
        

Generated by: LCOV version 2.0-1