LCOV - code coverage report
Current view: top level - src/domain/read - history.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 85.6 % 354 303
Test Date: 2026-04-20 19:54:24 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           64 : static int write_input_peer(TlWriter *w, const HistoryPeer *p) {
      56           64 :     switch (p->kind) {
      57           56 :     case HISTORY_PEER_SELF:
      58           56 :         tl_write_uint32(w, TL_inputPeerSelf);
      59           56 :         return 0;
      60            4 :     case HISTORY_PEER_USER:
      61            4 :         tl_write_uint32(w, TL_inputPeerUser);
      62            4 :         tl_write_int64 (w, p->peer_id);
      63            4 :         tl_write_int64 (w, p->access_hash);
      64            4 :         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            4 :     case HISTORY_PEER_CHANNEL:
      70            4 :         tl_write_uint32(w, TL_inputPeerChannel);
      71            4 :         tl_write_int64 (w, p->peer_id);
      72            4 :         tl_write_int64 (w, p->access_hash);
      73            4 :         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           64 : 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           64 :     tl_writer_init(&w);
      92           64 :     tl_write_uint32(&w, CRC_messages_getHistory);
      93           64 :     if (write_input_peer(&w, peer) != 0) {
      94            0 :         tl_writer_free(&w);
      95            0 :         return -1;
      96              :     }
      97           64 :     tl_write_int32 (&w, offset_id);
      98           64 :     tl_write_int32 (&w, 0);  /* offset_date */
      99           64 :     tl_write_int32 (&w, 0);  /* add_offset */
     100           64 :     tl_write_int32 (&w, limit);
     101           64 :     tl_write_int32 (&w, 0);  /* max_id */
     102           64 :     tl_write_int32 (&w, 0);  /* min_id */
     103           64 :     tl_write_int64 (&w, 0);  /* hash */
     104              : 
     105           64 :     int rc = -1;
     106           64 :     if (w.len <= cap) {
     107           64 :         memcpy(buf, w.data, w.len);
     108           64 :         *out_len = w.len;
     109           64 :         rc = 0;
     110              :     }
     111           64 :     tl_writer_free(&w);
     112           64 :     return rc;
     113              : }
     114              : 
     115              : /* Copy src into out->text with HISTORY_TEXT_MAX truncation. */
     116           22 : static void set_text(HistoryEntry *out, const char *src) {
     117           22 :     if (!src) { out->text[0] = '\0'; return; }
     118           22 :     size_t n = strlen(src);
     119           22 :     if (n >= HISTORY_TEXT_MAX) { n = HISTORY_TEXT_MAX - 1; out->truncated = 1; }
     120           22 :     memcpy(out->text, src, n);
     121           22 :     out->text[n] = '\0';
     122              : }
     123              : 
     124              : /* Read and discard a Vector<long>. Returns 0 on success. */
     125            2 : static int skip_long_vector(TlReader *r) {
     126            2 :     if (r->len - r->pos < 8) return -1;
     127            2 :     if (tl_read_uint32(r) != TL_vector) return -1;
     128            2 :     uint32_t n = tl_read_uint32(r);
     129            2 :     if (n > 4096) return -1; /* sanity cap */
     130            2 :     if (r->len - r->pos < (size_t)n * 8) return -1;
     131            5 :     for (uint32_t i = 0; i < n; i++) tl_read_int64(r);
     132            2 :     return 0;
     133              : }
     134              : 
     135              : /* Discard the remaining Vector<long>; return the first element (or 0). */
     136            1 : static int read_long_vector_first(TlReader *r, int64_t *first) {
     137            1 :     if (r->len - r->pos < 8) return -1;
     138            1 :     if (tl_read_uint32(r) != TL_vector) return -1;
     139            1 :     uint32_t n = tl_read_uint32(r);
     140            1 :     if (n > 4096) return -1;
     141            1 :     if (r->len - r->pos < (size_t)n * 8) return -1;
     142            1 :     if (first) *first = 0;
     143            2 :     for (uint32_t i = 0; i < n; i++) {
     144            1 :         int64_t v = tl_read_int64(r);
     145            1 :         if (i == 0 && first) *first = v;
     146              :     }
     147            1 :     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           22 : static int parse_service_action(TlReader *r, HistoryEntry *out) {
     154           22 :     out->is_service = 1;
     155           22 :     if (r->len - r->pos < 4) { set_text(out, "[service action truncated]"); return -1; }
     156           22 :     uint32_t acrc = tl_read_uint32(r);
     157              :     char buf[HISTORY_TEXT_MAX];
     158              : 
     159           22 :     switch (acrc) {
     160            1 :     case CRC_messageActionEmpty:
     161            1 :         set_text(out, "");
     162            1 :         return 0;
     163            1 :     case CRC_messageActionChatCreate: {
     164              :         /* title:string users:Vector<long> */
     165            2 :         RAII_STRING char *title = tl_read_string(r);
     166            1 :         if (!title) return -1;
     167            1 :         if (skip_long_vector(r) != 0) return -1;
     168            1 :         snprintf(buf, sizeof(buf), "created group '%s'", title);
     169            1 :         set_text(out, buf);
     170            1 :         return 0;
     171              :     }
     172            1 :     case CRC_messageActionChatEditTitle: {
     173              :         /* title:string */
     174            2 :         RAII_STRING char *title = tl_read_string(r);
     175            1 :         if (!title) return -1;
     176            1 :         snprintf(buf, sizeof(buf), "changed title to '%s'", title);
     177            1 :         set_text(out, buf);
     178            1 :         return 0;
     179              :     }
     180            1 :     case CRC_messageActionChatEditPhoto: {
     181              :         /* photo:Photo — skip via tl_skip_photo */
     182            1 :         if (tl_skip_photo(r) != 0) { set_text(out, "changed group photo"); return -1; }
     183            1 :         set_text(out, "changed group photo");
     184            1 :         return 0;
     185              :     }
     186            1 :     case CRC_messageActionChatDeletePhoto:
     187            1 :         set_text(out, "removed group photo");
     188            1 :         return 0;
     189            1 :     case CRC_messageActionChatAddUser: {
     190              :         /* users:Vector<long> */
     191            1 :         int64_t first = 0;
     192            1 :         if (read_long_vector_first(r, &first) != 0) return -1;
     193            1 :         snprintf(buf, sizeof(buf), "added @%lld", (long long)first);
     194            1 :         set_text(out, buf);
     195            1 :         return 0;
     196              :     }
     197            1 :     case CRC_messageActionChatDeleteUser: {
     198              :         /* user_id:long */
     199            1 :         if (r->len - r->pos < 8) return -1;
     200            1 :         int64_t uid = tl_read_int64(r);
     201            1 :         snprintf(buf, sizeof(buf), "removed @%lld", (long long)uid);
     202            1 :         set_text(out, buf);
     203            1 :         return 0;
     204              :     }
     205            1 :     case CRC_messageActionChatJoinedByLink: {
     206              :         /* inviter_id:long */
     207            1 :         if (r->len - r->pos < 8) return -1;
     208            1 :         (void)tl_read_int64(r);
     209            1 :         set_text(out, "joined via invite link");
     210            1 :         return 0;
     211              :     }
     212            1 :     case CRC_messageActionChannelCreate: {
     213              :         /* title:string */
     214            2 :         RAII_STRING char *title = tl_read_string(r);
     215            1 :         if (!title) return -1;
     216            1 :         snprintf(buf, sizeof(buf), "created channel '%s'", title);
     217            1 :         set_text(out, buf);
     218            1 :         return 0;
     219              :     }
     220            1 :     case CRC_messageActionChatMigrateTo: {
     221              :         /* channel_id:long */
     222            1 :         if (r->len - r->pos < 8) return -1;
     223            1 :         int64_t cid = tl_read_int64(r);
     224            1 :         snprintf(buf, sizeof(buf), "migrated to channel %lld", (long long)cid);
     225            1 :         set_text(out, buf);
     226            1 :         return 0;
     227              :     }
     228            1 :     case CRC_messageActionChannelMigrateFrom: {
     229              :         /* title:string chat_id:long */
     230            2 :         RAII_STRING char *title = tl_read_string(r);
     231            1 :         if (!title) return -1;
     232            1 :         if (r->len - r->pos < 8) return -1;
     233            1 :         int64_t cid = tl_read_int64(r);
     234            1 :         snprintf(buf, sizeof(buf), "migrated from group %lld '%s'",
     235              :                  (long long)cid, title);
     236            1 :         set_text(out, buf);
     237            1 :         return 0;
     238              :     }
     239            2 :     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            2 :         if (out->text[0] == '\0') set_text(out, "pinned message");
     243            2 :         return 0;
     244            1 :     case CRC_messageActionHistoryClear:
     245            1 :         set_text(out, "history cleared");
     246            1 :         return 0;
     247            2 :     case CRC_messageActionPhoneCall: {
     248              :         /* flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int */
     249            2 :         if (r->len - r->pos < 4 + 8) return -1;
     250            2 :         uint32_t pflags = tl_read_uint32(r);
     251            2 :         (void)tl_read_int64(r); /* call_id */
     252            2 :         const char *reason = "completed";
     253            2 :         if (pflags & 1u) {
     254            2 :             if (r->len - r->pos < 4) return -1;
     255            2 :             uint32_t rcrc = tl_read_uint32(r);
     256            2 :             switch (rcrc) {
     257            1 :             case CRC_phoneCallDiscardReasonMissed:     reason = "missed"; break;
     258            0 :             case CRC_phoneCallDiscardReasonDisconnect: reason = "disconnected"; break;
     259            1 :             case CRC_phoneCallDiscardReasonHangup:     reason = "hangup"; break;
     260            0 :             case CRC_phoneCallDiscardReasonBusy:       reason = "busy"; break;
     261            0 :             default: break;
     262              :             }
     263              :         }
     264            2 :         int duration = 0;
     265            2 :         if (pflags & (1u << 1)) {
     266            1 :             if (r->len - r->pos < 4) return -1;
     267            1 :             duration = tl_read_int32(r);
     268              :         }
     269            2 :         snprintf(buf, sizeof(buf), "called (duration %ds, %s)", duration, reason);
     270            2 :         set_text(out, buf);
     271            2 :         return 0;
     272              :     }
     273            1 :     case CRC_messageActionScreenshotTaken:
     274            1 :         set_text(out, "took screenshot");
     275            1 :         return 0;
     276            1 :     case CRC_messageActionCustomAction: {
     277              :         /* message:string — the raw action label chosen by the server. */
     278            2 :         RAII_STRING char *msg = tl_read_string(r);
     279            1 :         if (!msg) return -1;
     280            1 :         set_text(out, msg);
     281            1 :         return 0;
     282              :     }
     283            1 :     case CRC_messageActionGroupCall: {
     284              :         /* flags:# call:InputGroupCall duration:flags.0?int
     285              :          * inputGroupCall#d8aa840f id:long access_hash:long */
     286            1 :         if (r->len - r->pos < 4) return -1;
     287            1 :         uint32_t gflags = tl_read_uint32(r);
     288            1 :         if (r->len - r->pos < 4 + 16) return -1;
     289            1 :         tl_read_uint32(r); /* inputGroupCall crc */
     290            1 :         tl_read_int64(r);  /* id */
     291            1 :         tl_read_int64(r);  /* access_hash */
     292            1 :         if (gflags & 1u) {
     293            0 :             if (r->len - r->pos < 4) return -1;
     294            0 :             tl_read_int32(r); /* duration */
     295              :         }
     296            1 :         set_text(out, "started video chat");
     297            1 :         return 0;
     298              :     }
     299            1 :     case CRC_messageActionGroupCallScheduled: {
     300              :         /* call:InputGroupCall schedule_date:int */
     301            1 :         if (r->len - r->pos < 4 + 16 + 4) return -1;
     302            1 :         tl_read_uint32(r); tl_read_int64(r); tl_read_int64(r);
     303            1 :         int sched = tl_read_int32(r);
     304            1 :         snprintf(buf, sizeof(buf), "scheduled video chat for %d", sched);
     305            1 :         set_text(out, buf);
     306            1 :         return 0;
     307              :     }
     308            1 :     case CRC_messageActionInviteToGroupCall: {
     309              :         /* call:InputGroupCall users:Vector<long> */
     310            1 :         if (r->len - r->pos < 4 + 16) return -1;
     311            1 :         tl_read_uint32(r); tl_read_int64(r); tl_read_int64(r);
     312            1 :         if (skip_long_vector(r) != 0) return -1;
     313            1 :         set_text(out, "invited to video chat");
     314            1 :         return 0;
     315              :     }
     316            1 :     default:
     317            1 :         snprintf(buf, sizeof(buf), "[service action 0x%08x]", acrc);
     318            1 :         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            1 :         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           22 : int domain_history_parse_service(TlReader *r, HistoryEntry *out, uint32_t flags) {
     331           22 :     out->is_service = 1;
     332           22 :     if (flags & MSG_FLAG_HAS_FROM_ID) {
     333            0 :         if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     334              :     }
     335           22 :     if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     336           22 :     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            2 :         if (r->len - r->pos >= 12) {
     341              :             uint32_t rh_crc, rh_flags;
     342            2 :             memcpy(&rh_crc,   r->data + r->pos,     4);
     343            2 :             memcpy(&rh_flags, r->data + r->pos + 4, 4);
     344            2 :             if (rh_crc == 0xafbc09dbu && (rh_flags & (1u << 4))) {
     345              :                 int32_t pinned;
     346            2 :                 memcpy(&pinned, r->data + r->pos + 8, 4);
     347              :                 char buf[HISTORY_TEXT_MAX];
     348            2 :                 snprintf(buf, sizeof(buf), "pinned message %d", pinned);
     349            2 :                 set_text(out, buf);
     350              :             }
     351              :         }
     352            2 :         if (tl_skip_message_reply_header(r) != 0) { out->complex = 1; return -1; }
     353              :     }
     354           22 :     if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     355           22 :     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           22 :     memcpy(saved_text, out->text, sizeof(saved_text));
     360           22 :     int rc = parse_service_action(r, out);
     361           22 :     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           22 :     if (rc == 0 && (flags & (1u << 25))) {
     365            0 :         if (r->len - r->pos >= 4) tl_read_int32(r);
     366              :     }
     367           22 :     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         1044 : static int parse_message(TlReader *r, HistoryEntry *out) {
     375         1044 :     if (!tl_reader_ok(r)) return -1;
     376         1044 :     uint32_t crc = tl_read_uint32(r);
     377              : 
     378         1044 :     if (crc == TL_messageEmpty) {
     379            1 :         uint32_t flags = tl_read_uint32(r);
     380            1 :         out->id = tl_read_int32(r);
     381            1 :         if (flags & 1u) { tl_read_uint32(r); tl_read_int64(r); }
     382            1 :         return 0;
     383              :     }
     384              : 
     385         1043 :     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         1043 :     uint32_t flags = tl_read_uint32(r);
     391         1043 :     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         1043 :     if (crc == TL_message) flags2 = tl_read_uint32(r);
     395         1043 :     out->out = (flags & MSG_FLAG_OUT) ? 1 : 0;
     396         1043 :     out->id  = tl_read_int32(r);
     397              : 
     398         1043 :     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           21 :         return domain_history_parse_service(r, out, flags);
     402              :     }
     403              : 
     404              :     /* Pre-text fields (message is read AFTER these). */
     405         1022 :     if (flags & MSG_FLAG_HAS_FROM_ID) {
     406            0 :         if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     407              :     }
     408         1022 :     if (tl_skip_peer(r) != 0)         { out->complex = 1; return -1; }
     409         1022 :     if (flags & (1u << 28)) { /* saved_peer_id (layer 185+) */
     410            1 :         if (tl_skip_peer(r) != 0) { out->complex = 1; return -1; }
     411              :     }
     412         1022 :     if (flags & (1u << 2)) { /* fwd_from */
     413            2 :         if (tl_skip_message_fwd_header(r) != 0) { out->complex = 1; return -1; }
     414              :     }
     415         1022 :     if (flags & (1u << 11)) { /* via_bot_id */
     416            3 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     417            3 :         tl_read_int64(r);
     418              :     }
     419         1022 :     if (flags2 & (1u << 0)) { /* via_business_bot_id */
     420            1 :         if (r->len - r->pos < 8) { out->complex = 1; return -1; }
     421            1 :         tl_read_int64(r);
     422              :     }
     423         1022 :     if (flags & (1u << 3)) { /* reply_to */
     424            2 :         if (tl_skip_message_reply_header(r) != 0) { out->complex = 1; return -1; }
     425              :     }
     426         1022 :     if (r->len - r->pos < 4) { out->complex = 1; return -1; }
     427         1022 :     out->date = (int64_t)(int32_t)tl_read_int32(r);
     428              : 
     429              :     /* message:string — always present. */
     430         2044 :     RAII_STRING char *msg = tl_read_string(r);
     431         1022 :     if (msg) {
     432         1022 :         size_t n = strlen(msg);
     433         1022 :         if (n >= HISTORY_TEXT_MAX) { n = HISTORY_TEXT_MAX - 1; out->truncated = 1; }
     434         1022 :         memcpy(out->text, msg, n);
     435         1022 :         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         1022 :     if (flags & (1u << 9)) {
     448           12 :         MediaInfo mi = {0};
     449           12 :         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           12 :         out->media    = mi.kind;
     455           24 :         out->media_id = (mi.kind == MEDIA_PHOTO) ? mi.photo_id
     456           12 :                       : (mi.kind == MEDIA_DOCUMENT) ? mi.document_id : 0;
     457           12 :         out->media_dc = mi.dc_id;
     458           12 :         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         1022 :     if (flags & (1u << 6)) { /* reply_markup */
     466            0 :         if (tl_skip_reply_markup(r) != 0) { out->complex = 1; return -1; }
     467              :     }
     468         1022 :     if (flags & (1u << 7))  { /* entities */
     469            0 :         if (tl_skip_message_entities_vector(r) != 0) { out->complex = 1; return -1; }
     470              :     }
     471         1022 :     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         1022 :     if (flags & (1u << 23)) { /* replies */
     476            0 :         if (tl_skip_message_replies(r) != 0) { out->complex = 1; return -1; }
     477              :     }
     478         1022 :     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         1022 :     if (flags & (1u << 16)) { /* post_author */
     483            0 :         if (tl_skip_string(r) != 0) { out->complex = 1; return -1; }
     484              :     }
     485         1022 :     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         1022 :     if (flags & (1u << 20)) { /* reactions */
     490            0 :         if (tl_skip_message_reactions(r) != 0) { out->complex = 1; return -1; }
     491              :     }
     492         1022 :     if (flags & (1u << 22)) { /* restriction_reason */
     493            0 :         if (tl_skip_restriction_reason_vector(r) != 0) {
     494            0 :             out->complex = 1; return -1;
     495              :         }
     496              :     }
     497         1022 :     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         1022 :     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         1022 :     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         1022 :     if (flags2 & (1u << 3)) { /* factcheck */
     510            0 :         if (tl_skip_factcheck(r) != 0) { out->complex = 1; return -1; }
     511              :     }
     512         1022 :     return 0;
     513              : }
     514              : 
     515           64 : 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           64 :     if (!cfg || !s || !t || !peer || !out || !out_count || limit <= 0) return -1;
     521           64 :     *out_count = 0;
     522              : 
     523              :     uint8_t query[128];
     524           64 :     size_t qlen = 0;
     525           64 :     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           64 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(262144);
     531           64 :     if (!resp) return -1;
     532           64 :     size_t resp_len = 0;
     533           64 :     if (api_call(cfg, s, t, query, qlen, resp, 262144, &resp_len) != 0) {
     534            1 :         logger_log(LOG_ERROR, "history: api_call failed");
     535            1 :         return -1;
     536              :     }
     537           63 :     if (resp_len < 8) return -1;
     538              : 
     539              :     uint32_t top;
     540           63 :     memcpy(&top, resp, 4);
     541           63 :     if (top == TL_rpc_error) {
     542              :         RpcError err;
     543            0 :         rpc_parse_error(resp, resp_len, &err);
     544            0 :         logger_log(LOG_ERROR, "history: RPC error %d: %s",
     545              :                    err.error_code, err.error_msg);
     546            0 :         return -1;
     547              :     }
     548              : 
     549           63 :     if (top != TL_messages_messages &&
     550           11 :         top != TL_messages_messagesSlice &&
     551            3 :         top != TL_messages_channelMessages) {
     552            0 :         logger_log(LOG_ERROR, "history: unexpected top 0x%08x", top);
     553            0 :         return -1;
     554              :     }
     555              : 
     556           63 :     TlReader r = tl_reader_init(resp, resp_len);
     557           63 :     tl_read_uint32(&r); /* top */
     558              : 
     559              :     /* messagesSlice/channelMessages prepend some counters we skip. */
     560           63 :     if (top == TL_messages_messagesSlice) {
     561            8 :         tl_read_uint32(&r); /* flags */
     562            8 :         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           55 :     } else if (top == TL_messages_channelMessages) {
     567            3 :         tl_read_uint32(&r); /* flags */
     568            3 :         tl_read_int32 (&r); /* pts */
     569            3 :         tl_read_int32 (&r); /* count */
     570              :         /* optional offset_id_offset (flags.2) not read */
     571              :     }
     572              : 
     573           63 :     uint32_t vec = tl_read_uint32(&r);
     574           63 :     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           63 :     uint32_t count = tl_read_uint32(&r);
     580           63 :     int written = 0;
     581         1107 :     for (uint32_t i = 0; i < count && written < limit; i++) {
     582         1044 :         HistoryEntry e = {0};
     583         1044 :         int rc2 = parse_message(&r, &e);
     584         1044 :         if (e.id != 0 || e.text[0] != '\0') {
     585         1044 :             out[written++] = e;
     586              :         }
     587         1044 :         if (rc2 != 0) break; /* could not advance past message — stop */
     588              :     }
     589           63 :     *out_count = written;
     590           63 :     return 0;
     591              : }
     592              : 
     593           54 : 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           54 :     HistoryPeer self = { .kind = HISTORY_PEER_SELF };
     598           54 :     return domain_get_history(cfg, s, t, &self, offset_id, limit, out, out_count);
     599              : }
        

Generated by: LCOV version 2.0-1