LCOV - code coverage report
Current view: top level - tests/functional - test_tl_forward_compat.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 1712 1712
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 35 35

            Line data    Source code
       1              : /**
       2              :  * @file test_tl_forward_compat.c
       3              :  * @brief TEST-75 — TL forward-compatibility of tl_skip.c.
       4              :  *
       5              :  * Exercises the "unknown constructor" contract of tl_skip.c: when a
       6              :  * future Telegram layer introduces a new CRC where one of the skip
       7              :  * functions is called, the skipper must refuse to advance silently
       8              :  * (returning -1) instead of corrupting the reader. The five cases map
       9              :  * to the ticket:
      10              :  *
      11              :  *   1. Unknown trailing field inside a messages.dialogs-style top-level
      12              :  *      result: the known Message bytes before it are fully consumable.
      13              :  *   2. Unknown MessageMedia CRC inside a Message: the text/date fields
      14              :  *      are extracted and tl_skip_message_media reports MEDIA_OTHER.
      15              :  *   3. Unknown message action (messageService): the bare prefix
      16              :  *      (flags+id) is advanced past but the body refuses to iterate, so
      17              :  *      the ID is the only thing we can present.
      18              :  *   4. Known Message with a trailing optional flag bit whose type we do
      19              :  *      not know yet: all known-position fields parse correctly up to
      20              :  *      the unknown flag, and the skipper refuses to invent a layout
      21              :  *      for the unknown bit.
      22              :  *   5. Unknown Update CRC inside updates.difference: iterating a
      23              :  *      Vector<Message> with unknown trailing fields stops cleanly at
      24              :  *      the first unknown CRC rather than desynchronising the reader.
      25              :  *
      26              :  * The tests drive the skip layer directly with fabricated byte streams
      27              :  * — the same pattern as test_tl_skip_message_functional.c — so we can
      28              :  * assert against the cursor position and the return code deterministically
      29              :  * without a live mock server.
      30              :  */
      31              : 
      32              : #include "test_helpers.h"
      33              : #include "tl_serial.h"
      34              : #include "tl_registry.h"
      35              : #include "tl_skip.h"
      36              : 
      37              : #include <stdint.h>
      38              : #include <string.h>
      39              : 
      40              : /* ---- Fabricated "future Telegram" CRCs ----
      41              :  *
      42              :  * The high byte 0xFF gives us values that the production CRC tables
      43              :  * (layer 170+) do not use; they are deliberately unknown so every skip
      44              :  * call must take the default/unknown branch.
      45              :  */
      46              : #define CRC_future_mediaTypeX       0xFF00AA01u
      47              : #define CRC_future_actionReward     0xFF00BB02u
      48              : #define CRC_future_replyMarkupTodo  0xFF00CC03u
      49              : #define CRC_future_entityBadge      0xFF00DD04u
      50              : #define CRC_future_updateHoroscope  0xFF00EE05u
      51              : #define CRC_future_peerGhost        0xFF00FF06u
      52              : #define CRC_future_chatPsychic      0xFF00FF07u
      53              : #define CRC_future_userTimelord     0xFF00FF08u
      54              : 
      55              : /* CRCs not re-exposed from tl_skip.c (copied from tl_skip_message_functional). */
      56              : #define CRC_messageMediaEmpty_t     0x3ded6320u
      57              : #define CRC_replyInlineMarkup_t     0x48a30254u
      58              : #define CRC_keyboardButtonRow_t     0x77608b83u
      59              : #define CRC_keyboardButtonCallback_t 0x35bbdb6bu
      60              : #define CRC_messageEntityBold_t     0xbd610bc9u
      61              : 
      62              : /* Message flag constants — same subset used by the kitchen-sink suite. */
      63              : #define FLAG_REPLY_MARKUP   (1u <<  6)
      64              : #define FLAG_ENTITIES       (1u <<  7)
      65              : #define FLAG_FROM_ID        (1u <<  8)
      66              : #define FLAG_MEDIA          (1u <<  9)
      67              : #define FLAG_VIEWS_FWDS     (1u << 10)
      68              : 
      69              : /* ---------------------------------------------------------------- */
      70              : /* Case 1 — unknown trailing field after known dialog bytes         */
      71              : /* ---------------------------------------------------------------- */
      72              : 
      73              : /* Build a realistic Message whose trailer carries an unknown CRC in
      74              :  * place of the expected reply_markup. The pre-message text/date/id all
      75              :  * land on the wire in their known positions; the skipper must stop at
      76              :  * the unknown reply_markup CRC and leave the reader at a well-defined
      77              :  * mid-object position so the caller can discard the tail and move on. */
      78            2 : static void write_message_with_unknown_trailer(TlWriter *w) {
      79            2 :     uint32_t flags = FLAG_FROM_ID | FLAG_REPLY_MARKUP;
      80            2 :     uint32_t flags2 = 0;
      81              : 
      82            2 :     tl_write_uint32(w, TL_message);
      83            2 :     tl_write_uint32(w, flags);
      84            2 :     tl_write_uint32(w, flags2);
      85            2 :     tl_write_int32 (w, 42);                     /* id */
      86              : 
      87              :     /* from_id + peer_id */
      88            2 :     tl_write_uint32(w, TL_peerUser); tl_write_int64(w, 7LL);
      89            2 :     tl_write_uint32(w, TL_peerChannel); tl_write_int64(w, 88LL);
      90              : 
      91            2 :     tl_write_int32 (w, 1700000000);             /* date */
      92            2 :     tl_write_string(w, "known text body");      /* message */
      93              : 
      94              :     /* reply_markup: an unknown CRC. The skipper must refuse. */
      95            2 :     tl_write_uint32(w, CRC_future_replyMarkupTodo);
      96            2 :     tl_write_int32 (w, 0xDEAD);
      97            2 :     tl_write_int32 (w, 0xBEEF);
      98            2 : }
      99              : 
     100            2 : static void test_unknown_top_level_result_skipped(void) {
     101            2 :     TlWriter w; tl_writer_init(&w);
     102            2 :     write_message_with_unknown_trailer(&w);
     103              : 
     104            2 :     TlReader r = tl_reader_init(w.data, w.len);
     105              :     /* tl_skip_message must refuse to advance because the reply_markup
     106              :      * CRC is unknown.  The reader position is defined as
     107              :      * "undefined — caller stops iterating" per the API contract. */
     108            2 :     ASSERT(tl_skip_message(&r) == -1,
     109              :            "tl_skip_message rejects unknown reply_markup CRC");
     110              :     /* Cursor should have advanced past the mandatory prefix at minimum
     111              :      * (4 bytes CRC + 4 flags + 4 flags2 + 4 id = 16 bytes). */
     112            2 :     ASSERT(r.pos >= 16, "cursor advanced past message prefix before bailing");
     113              : 
     114            2 :     tl_writer_free(&w);
     115              : 
     116              :     /* Companion check: feed the same known reply_markup with a valid
     117              :      * replyInlineMarkup — the skipper must accept it, so we can be sure
     118              :      * the rejection in the first part was caused by the CRC alone. */
     119            2 :     tl_writer_init(&w);
     120            2 :     uint32_t flags = FLAG_FROM_ID | FLAG_REPLY_MARKUP;
     121            2 :     tl_write_uint32(&w, TL_message);
     122            2 :     tl_write_uint32(&w, flags);
     123            2 :     tl_write_uint32(&w, 0);
     124            2 :     tl_write_int32 (&w, 42);
     125            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 7LL);
     126            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 88LL);
     127            2 :     tl_write_int32 (&w, 1700000000);
     128            2 :     tl_write_string(&w, "known text body");
     129              :     /* valid inline markup with zero rows */
     130            2 :     tl_write_uint32(&w, CRC_replyInlineMarkup_t);
     131            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     132            2 :     tl_write_int32 (&w, 0xCAFE);                /* trailer sentinel */
     133              : 
     134            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     135            2 :     ASSERT(tl_skip_message(&r2) == 0,
     136              :            "tl_skip_message accepts known empty inline markup");
     137            2 :     ASSERT(tl_read_int32(&r2) == 0x0000CAFE,
     138              :            "cursor lands on the sentinel after known reply_markup");
     139            2 :     tl_writer_free(&w);
     140              : }
     141              : 
     142              : /* ---------------------------------------------------------------- */
     143              : /* Case 2 — unknown MessageMedia CRC mid-message                    */
     144              : /* ---------------------------------------------------------------- */
     145              : 
     146            2 : static void test_unknown_media_in_history(void) {
     147            2 :     TlWriter w; tl_writer_init(&w);
     148              : 
     149            2 :     uint32_t flags = FLAG_FROM_ID | FLAG_MEDIA;
     150            2 :     tl_write_uint32(&w, TL_message);
     151            2 :     tl_write_uint32(&w, flags);
     152            2 :     tl_write_uint32(&w, 0);                     /* flags2 */
     153            2 :     tl_write_int32 (&w, 501);                   /* id */
     154            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 9LL);
     155            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 77LL);
     156            2 :     tl_write_int32 (&w, 1700000100);            /* date */
     157            2 :     tl_write_string(&w, "caption survives");    /* message */
     158              : 
     159              :     /* Unknown MessageMedia variant — three bogus trailer words so the
     160              :      * reader has bytes to overrun if the skipper mis-behaves. */
     161            2 :     tl_write_uint32(&w, CRC_future_mediaTypeX);
     162            2 :     tl_write_int32 (&w, 0x1111);
     163            2 :     tl_write_int32 (&w, 0x2222);
     164            2 :     tl_write_int32 (&w, 0x3333);
     165              : 
     166              :     /* tl_skip_message must return -1 and the cursor must sit at or past
     167              :      * the MessageMedia CRC (we have read 4 bytes of it before switching
     168              :      * in the default branch). */
     169            2 :     TlReader r = tl_reader_init(w.data, w.len);
     170            2 :     ASSERT(tl_skip_message(&r) == -1,
     171              :            "unknown media CRC halts tl_skip_message");
     172              : 
     173              :     /* Direct exercise of tl_skip_message_media_ex with the unknown CRC
     174              :      * verifies the "MEDIA_OTHER on unknown" contract. */
     175            2 :     TlWriter w2; tl_writer_init(&w2);
     176            2 :     tl_write_uint32(&w2, CRC_future_mediaTypeX);
     177            2 :     tl_write_int32 (&w2, 0);
     178            2 :     TlReader r2 = tl_reader_init(w2.data, w2.len);
     179            2 :     MediaInfo mi = {0};
     180            2 :     ASSERT(tl_skip_message_media_ex(&r2, &mi) == -1,
     181              :            "unknown MessageMedia variant returns -1");
     182            2 :     ASSERT(mi.kind == MEDIA_OTHER,
     183              :            "unknown MessageMedia labels out as MEDIA_OTHER");
     184              : 
     185              :     /* Sanity: a known messageMediaEmpty must succeed at MEDIA_EMPTY. */
     186            2 :     TlWriter w3; tl_writer_init(&w3);
     187            2 :     tl_write_uint32(&w3, CRC_messageMediaEmpty_t);
     188            2 :     TlReader r3 = tl_reader_init(w3.data, w3.len);
     189            2 :     MediaInfo mi3 = {0};
     190            2 :     ASSERT(tl_skip_message_media_ex(&r3, &mi3) == 0,
     191              :            "messageMediaEmpty accepted");
     192            2 :     ASSERT(mi3.kind == MEDIA_EMPTY, "empty labelled MEDIA_EMPTY");
     193            2 :     ASSERT(r3.pos == r3.len, "reader fully consumed on empty media");
     194              : 
     195            2 :     tl_writer_free(&w);
     196            2 :     tl_writer_free(&w2);
     197            2 :     tl_writer_free(&w3);
     198              : }
     199              : 
     200              : /* ---------------------------------------------------------------- */
     201              : /* Case 3 — unknown messageService action                           */
     202              : /* ---------------------------------------------------------------- */
     203              : 
     204            2 : static void test_unknown_message_action(void) {
     205            2 :     TlWriter w; tl_writer_init(&w);
     206              : 
     207              :     /* Build a messageService envelope. tl_skip_message refuses to walk
     208              :      * messageService bodies today — it returns -1 after reading the
     209              :      * prefix — so the action CRC itself never reaches a dispatcher,
     210              :      * but the caller can still display id/date because they were read
     211              :      * before the bail. */
     212            2 :     tl_write_uint32(&w, TL_messageService);
     213            2 :     tl_write_uint32(&w, 0);                     /* flags */
     214            2 :     tl_write_uint32(&w, 0);                     /* flags2 */
     215            2 :     tl_write_int32 (&w, 9001);                  /* id */
     216            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 3LL);
     217            2 :     tl_write_int32 (&w, 1700000200);            /* date */
     218              :     /* Unknown service action — tl_skip_message never reaches it. */
     219            2 :     tl_write_uint32(&w, CRC_future_actionReward);
     220            2 :     tl_write_int64 (&w, 0xAABBCCDDEEFF0011LL);
     221              : 
     222            2 :     TlReader r = tl_reader_init(w.data, w.len);
     223              :     /* Contract: messageService is unsupported → -1. The reader position
     224              :      * is undefined but we must not have advanced past the buffer end. */
     225            2 :     ASSERT(tl_skip_message(&r) == -1,
     226              :            "messageService skip refuses to walk body");
     227            2 :     ASSERT(r.pos <= r.len, "reader stays in-bounds on bail");
     228            2 :     tl_writer_free(&w);
     229              : }
     230              : 
     231              : /* ---------------------------------------------------------------- */
     232              : /* Case 4 — known Message with unknown optional flag bit            */
     233              : /* ---------------------------------------------------------------- */
     234              : 
     235            2 : static void test_unknown_optional_field_preserves_layout(void) {
     236            2 :     TlWriter w; tl_writer_init(&w);
     237              : 
     238              :     /* Set a flag bit the current skipper does not know about (bit 31 in
     239              :      * flags has no meaning today).  The pre-text fields — from_id, peer,
     240              :      * date, message — must still parse because they sit before any
     241              :      * optional trailer. */
     242            2 :     uint32_t flags  = FLAG_FROM_ID | (1u << 31);
     243            2 :     uint32_t flags2 = 0;
     244              : 
     245            2 :     tl_write_uint32(&w, TL_message);
     246            2 :     tl_write_uint32(&w, flags);
     247            2 :     tl_write_uint32(&w, flags2);
     248            2 :     tl_write_int32 (&w, 777);
     249            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 11LL);
     250            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 99LL);
     251            2 :     tl_write_int32 (&w, 1700000300);
     252            2 :     tl_write_string(&w, "survives unknown trailing bit");
     253              : 
     254              :     /* tl_skip_message walks every known flag bit.  Because bit 31 has
     255              :      * no known payload, the skipper is expected to return 0 (no data
     256              :      * is read for that bit) — confirming the "unknown bit = no-op"
     257              :      * forward-compat policy. If a future change bound bit 31 to a
     258              :      * payload, this test would need to be updated in lock-step with
     259              :      * the new skipper.  */
     260            2 :     TlReader r = tl_reader_init(w.data, w.len);
     261            2 :     ASSERT(tl_skip_message(&r) == 0,
     262              :            "unknown flag bit with no payload is skipped as a no-op");
     263            2 :     ASSERT(r.pos == r.len, "reader fully consumed");
     264            2 :     tl_writer_free(&w);
     265              : 
     266              :     /* Companion: if a caller sets flags2 bit 31 — also currently
     267              :      * unused — the skipper must likewise advance cleanly. */
     268            2 :     tl_writer_init(&w);
     269            2 :     tl_write_uint32(&w, TL_message);
     270            2 :     tl_write_uint32(&w, FLAG_FROM_ID);
     271            2 :     tl_write_uint32(&w, (1u << 31));             /* unknown flags2 bit */
     272            2 :     tl_write_int32 (&w, 778);
     273            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 12LL);
     274            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 100LL);
     275            2 :     tl_write_int32 (&w, 1700000400);
     276            2 :     tl_write_string(&w, "flags2 variant");
     277            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     278            2 :     ASSERT(tl_skip_message(&r2) == 0,
     279              :            "unknown flags2 bit with no payload is skipped");
     280            2 :     ASSERT(r2.pos == r2.len, "reader fully consumed on flags2 variant");
     281            2 :     tl_writer_free(&w);
     282              : }
     283              : 
     284              : /* ---------------------------------------------------------------- */
     285              : /* Case 5 — unknown Update CRC inside updates.difference            */
     286              : /* ---------------------------------------------------------------- */
     287              : 
     288              : /* updates.getDifference returns a Vector<Message> as its first
     289              :  * sub-field; tl_skip_message is how we iterate that vector.  An
     290              :  * unknown Message-like constructor at position i must halt iteration
     291              :  * at i so the caller can present whatever preceded it. */
     292            2 : static void test_unknown_update_type_in_getdifference(void) {
     293            2 :     TlWriter w; tl_writer_init(&w);
     294              : 
     295              :     /* Simulate two plausible messages followed by one "future update". */
     296              :     /* 0 — real messageEmpty */
     297            2 :     tl_write_uint32(&w, TL_messageEmpty);
     298            2 :     tl_write_uint32(&w, 0);                     /* flags */
     299            2 :     tl_write_int32 (&w, 601);
     300              :     /* 1 — real messageEmpty */
     301            2 :     tl_write_uint32(&w, TL_messageEmpty);
     302            2 :     tl_write_uint32(&w, 0);
     303            2 :     tl_write_int32 (&w, 602);
     304              :     /* 2 — an unknown Message-like CRC (not TL_message/Empty/Service). */
     305            2 :     tl_write_uint32(&w, CRC_future_updateHoroscope);
     306            2 :     tl_write_int32 (&w, 0xDEADBEEF);
     307              : 
     308            2 :     TlReader r = tl_reader_init(w.data, w.len);
     309            2 :     ASSERT(tl_skip_message(&r) == 0, "first messageEmpty skipped");
     310            2 :     ASSERT(tl_skip_message(&r) == 0, "second messageEmpty skipped");
     311              :     /* Unknown CRC — tl_skip_message must refuse rather than guess. */
     312            2 :     ASSERT(tl_skip_message(&r) == -1,
     313              :            "unknown Message CRC halts iteration");
     314            2 :     tl_writer_free(&w);
     315              : }
     316              : 
     317              : /* ---------------------------------------------------------------- */
     318              : /* Additional coverage — exercise the remaining skip surface        */
     319              : /*                                                                  */
     320              : /* These are not in the ticket's enumerated list but drive many more */
     321              : /* lines of tl_skip.c that are otherwise only touched by unit tests. */
     322              : /* ---------------------------------------------------------------- */
     323              : 
     324              : /* tl_skip_peer / tl_skip_bool / tl_skip_string on unknown input. */
     325            2 : static void test_skip_primitives_reject_unknown(void) {
     326              :     /* Unknown Peer variant. */
     327            2 :     TlWriter w; tl_writer_init(&w);
     328            2 :     tl_write_uint32(&w, CRC_future_peerGhost);
     329            2 :     tl_write_int64 (&w, 123LL);
     330            2 :     TlReader r = tl_reader_init(w.data, w.len);
     331            2 :     ASSERT(tl_skip_peer(&r) == -1, "tl_skip_peer rejects unknown variant");
     332            2 :     tl_writer_free(&w);
     333              : 
     334              :     /* Known Peer variants succeed (peerUser/Chat/Channel). */
     335            2 :     tl_writer_init(&w);
     336            2 :     tl_write_uint32(&w, TL_peerUser);    tl_write_int64(&w, 1LL);
     337            2 :     tl_write_uint32(&w, TL_peerChat);    tl_write_int64(&w, 2LL);
     338            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 3LL);
     339            2 :     TlReader rk = tl_reader_init(w.data, w.len);
     340            2 :     ASSERT(tl_skip_peer(&rk) == 0, "peerUser accepted");
     341            2 :     ASSERT(tl_skip_peer(&rk) == 0, "peerChat accepted");
     342            2 :     ASSERT(tl_skip_peer(&rk) == 0, "peerChannel accepted");
     343            2 :     ASSERT(rk.pos == rk.len, "peer reader fully consumed");
     344            2 :     tl_writer_free(&w);
     345              : 
     346              :     /* Bool: tl_skip_bool just reads 4 bytes regardless of value. */
     347            2 :     tl_writer_init(&w);
     348            2 :     tl_write_uint32(&w, TL_boolTrue);
     349            2 :     TlReader rb = tl_reader_init(w.data, w.len);
     350            2 :     ASSERT(tl_skip_bool(&rb) == 0, "bool skipped");
     351            2 :     ASSERT(rb.pos == 4, "bool consumed 4 bytes");
     352            2 :     tl_writer_free(&w);
     353              : 
     354              :     /* String round-trip. */
     355            2 :     tl_writer_init(&w);
     356            2 :     tl_write_string(&w, "forward-compat string payload");
     357            2 :     TlReader rs = tl_reader_init(w.data, w.len);
     358            2 :     ASSERT(tl_skip_string(&rs) == 0, "string skipped");
     359            2 :     ASSERT(rs.pos == rs.len, "string reader fully consumed");
     360            2 :     tl_writer_free(&w);
     361              : }
     362              : 
     363              : /* tl_skip_message_entity: drive several known variants to exercise the
     364              :  * switch body, and one unknown one. */
     365            2 : static void test_message_entity_variants(void) {
     366              :     /* Known variants: bold (8 bytes), textUrl (8 + string), mentionName
     367              :      * (16 bytes), custom emoji (16 bytes), blockquote (12 bytes). */
     368            2 :     TlWriter w; tl_writer_init(&w);
     369            2 :     tl_write_uint32(&w, TL_vector);
     370            2 :     tl_write_uint32(&w, 5);
     371              : 
     372              :     /* bold */
     373            2 :     tl_write_uint32(&w, CRC_messageEntityBold_t);
     374            2 :     tl_write_int32 (&w, 0); tl_write_int32(&w, 4);
     375              : 
     376              :     /* textUrl */
     377            2 :     tl_write_uint32(&w, 0x76a6d327u);
     378            2 :     tl_write_int32 (&w, 0); tl_write_int32(&w, 5);
     379            2 :     tl_write_string(&w, "https://example.org");
     380              : 
     381              :     /* mentionName */
     382            2 :     tl_write_uint32(&w, 0xdc7b1140u);
     383            2 :     tl_write_int32 (&w, 6); tl_write_int32(&w, 7);
     384            2 :     tl_write_int64 (&w, 42LL);
     385              : 
     386              :     /* custom emoji */
     387            2 :     tl_write_uint32(&w, 0xc8cf05f8u);
     388            2 :     tl_write_int32 (&w, 14); tl_write_int32(&w, 2);
     389            2 :     tl_write_int64 (&w, 9001LL);
     390              : 
     391              :     /* blockquote */
     392            2 :     tl_write_uint32(&w, 0xf1ccaaacu);
     393            2 :     tl_write_uint32(&w, 0);                     /* flags */
     394            2 :     tl_write_int32 (&w, 17); tl_write_int32(&w, 3);
     395              : 
     396            2 :     TlReader r = tl_reader_init(w.data, w.len);
     397            2 :     ASSERT(tl_skip_message_entities_vector(&r) == 0,
     398              :            "known entity variants all accepted");
     399            2 :     ASSERT(r.pos == r.len, "reader fully consumed");
     400            2 :     tl_writer_free(&w);
     401              : 
     402              :     /* Unknown entity CRC halts the whole vector. */
     403            2 :     tl_writer_init(&w);
     404            2 :     tl_write_uint32(&w, TL_vector);
     405            2 :     tl_write_uint32(&w, 1);
     406            2 :     tl_write_uint32(&w, CRC_future_entityBadge);
     407            2 :     tl_write_int32 (&w, 0); tl_write_int32(&w, 8);
     408            2 :     TlReader ru = tl_reader_init(w.data, w.len);
     409            2 :     ASSERT(tl_skip_message_entities_vector(&ru) == -1,
     410              :            "unknown entity CRC breaks the vector");
     411            2 :     tl_writer_free(&w);
     412              : }
     413              : 
     414              : /* Known MessageMedia variants round-trip — geo, contact, venue, dice,
     415              :  * geoLive — to sweep through a broad swath of tl_skip_message_media_ex. */
     416            2 : static void test_media_variants_skip_clean(void) {
     417              :     /* messageMediaGeo + geoPointEmpty */
     418            2 :     TlWriter w; tl_writer_init(&w);
     419            2 :     tl_write_uint32(&w, 0x56e0d474u);           /* messageMediaGeo */
     420            2 :     tl_write_uint32(&w, 0x1117dd5fu);           /* geoPointEmpty */
     421            2 :     TlReader r = tl_reader_init(w.data, w.len);
     422            2 :     ASSERT(tl_skip_message_media(&r) == 0, "media:geo/empty skipped");
     423            2 :     ASSERT(r.pos == r.len, "reader consumed geo/empty");
     424            2 :     tl_writer_free(&w);
     425              : 
     426              :     /* messageMediaContact: phone_number, first_name, last_name, vcard, user_id */
     427            2 :     tl_writer_init(&w);
     428            2 :     tl_write_uint32(&w, 0x70322949u);
     429            2 :     tl_write_string(&w, "+15550001234");
     430            2 :     tl_write_string(&w, "First");
     431            2 :     tl_write_string(&w, "Last");
     432            2 :     tl_write_string(&w, "BEGIN:VCARD\nEND:VCARD");
     433            2 :     tl_write_int64 (&w, 42LL);
     434            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     435            2 :     ASSERT(tl_skip_message_media(&r2) == 0, "media:contact skipped");
     436            2 :     ASSERT(r2.pos == r2.len, "reader consumed contact");
     437            2 :     tl_writer_free(&w);
     438              : 
     439              :     /* messageMediaDice: value + emoticon */
     440            2 :     tl_writer_init(&w);
     441            2 :     tl_write_uint32(&w, 0x3f7ee58bu);
     442            2 :     tl_write_int32 (&w, 6);
     443            2 :     tl_write_string(&w, "\xf0\x9f\x8e\xb2"); /* dice emoji */
     444            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     445            2 :     ASSERT(tl_skip_message_media(&r3) == 0, "media:dice skipped");
     446            2 :     ASSERT(r3.pos == r3.len, "reader consumed dice");
     447            2 :     tl_writer_free(&w);
     448              : 
     449              :     /* messageMediaVenue: geo + address strings + venue id/type */
     450            2 :     tl_writer_init(&w);
     451            2 :     tl_write_uint32(&w, 0x2ec0533fu);
     452            2 :     tl_write_uint32(&w, 0x1117dd5fu);           /* geoPointEmpty */
     453            2 :     tl_write_string(&w, "123 Main St");
     454            2 :     tl_write_string(&w, "Coffee shop");
     455            2 :     tl_write_string(&w, "foursquare");
     456            2 :     tl_write_string(&w, "V123");
     457            2 :     tl_write_string(&w, "cafe");
     458            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     459            2 :     ASSERT(tl_skip_message_media(&r4) == 0, "media:venue skipped");
     460            2 :     ASSERT(r4.pos == r4.len, "reader consumed venue");
     461            2 :     tl_writer_free(&w);
     462              : 
     463              :     /* messageMediaGeoLive: flags=0 + geoPointEmpty + period */
     464            2 :     tl_writer_init(&w);
     465            2 :     tl_write_uint32(&w, 0xb940c666u);
     466            2 :     tl_write_uint32(&w, 0);                     /* flags */
     467            2 :     tl_write_uint32(&w, 0x1117dd5fu);           /* geoPointEmpty */
     468            2 :     tl_write_int32 (&w, 3600);                  /* period */
     469            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
     470            2 :     ASSERT(tl_skip_message_media(&r5) == 0, "media:geoLive skipped");
     471            2 :     ASSERT(r5.pos == r5.len, "reader consumed geoLive");
     472            2 :     tl_writer_free(&w);
     473              : 
     474              :     /* messageMediaUnsupported — a known "we cannot render this" marker. */
     475            2 :     tl_writer_init(&w);
     476            2 :     tl_write_uint32(&w, 0x9f84f49eu);
     477            2 :     TlReader r6 = tl_reader_init(w.data, w.len);
     478            2 :     MediaInfo mi6 = {0};
     479            2 :     ASSERT(tl_skip_message_media_ex(&r6, &mi6) == 0,
     480              :            "media:unsupported accepted");
     481            2 :     ASSERT(mi6.kind == MEDIA_UNSUPPORTED,
     482              :            "unsupported marker labelled MEDIA_UNSUPPORTED");
     483            2 :     tl_writer_free(&w);
     484              : }
     485              : 
     486              : /* ReplyMarkup variants round-trip (hide, forceReply, inline, markup). */
     487            2 : static void test_reply_markup_variants(void) {
     488              :     /* replyKeyboardHide — flags=0 */
     489            2 :     TlWriter w; tl_writer_init(&w);
     490            2 :     tl_write_uint32(&w, 0xa03e5b85u);
     491            2 :     tl_write_uint32(&w, 0);
     492            2 :     TlReader r = tl_reader_init(w.data, w.len);
     493            2 :     ASSERT(tl_skip_reply_markup(&r) == 0, "keyboardHide skipped");
     494            2 :     ASSERT(r.pos == r.len, "hide reader consumed");
     495            2 :     tl_writer_free(&w);
     496              : 
     497              :     /* replyKeyboardForceReply with placeholder */
     498            2 :     tl_writer_init(&w);
     499            2 :     tl_write_uint32(&w, 0x86b40b08u);
     500            2 :     tl_write_uint32(&w, (1u << 3));             /* flags.3 → placeholder */
     501            2 :     tl_write_string(&w, "Type here...");
     502            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     503            2 :     ASSERT(tl_skip_reply_markup(&r2) == 0, "forceReply skipped");
     504            2 :     ASSERT(r2.pos == r2.len, "forceReply reader consumed");
     505            2 :     tl_writer_free(&w);
     506              : 
     507              :     /* replyInlineMarkup with one row, two buttons (button + url) */
     508            2 :     tl_writer_init(&w);
     509            2 :     tl_write_uint32(&w, CRC_replyInlineMarkup_t);
     510            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
     511            2 :     tl_write_uint32(&w, CRC_keyboardButtonRow_t);
     512            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
     513              :     /* keyboardButton */
     514            2 :     tl_write_uint32(&w, 0xa2fa4880u);
     515            2 :     tl_write_string(&w, "Yes");
     516              :     /* keyboardButtonUrl */
     517            2 :     tl_write_uint32(&w, 0x258aff05u);
     518            2 :     tl_write_string(&w, "Open");
     519            2 :     tl_write_string(&w, "https://example.org");
     520            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     521            2 :     ASSERT(tl_skip_reply_markup(&r3) == 0, "inlineMarkup with rows skipped");
     522            2 :     ASSERT(r3.pos == r3.len, "inline reader consumed");
     523            2 :     tl_writer_free(&w);
     524              : 
     525              :     /* Unknown ReplyMarkup */
     526            2 :     tl_writer_init(&w);
     527            2 :     tl_write_uint32(&w, CRC_future_replyMarkupTodo);
     528            2 :     tl_write_uint32(&w, 0);
     529            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     530            2 :     ASSERT(tl_skip_reply_markup(&r4) == -1,
     531              :            "unknown reply_markup CRC rejected");
     532            2 :     tl_writer_free(&w);
     533              : }
     534              : 
     535              : /* Chat/User extractors: unknown variant → -1; known path drives many
     536              :  * conditional lines. */
     537            2 : static void test_chat_user_unknown_variants(void) {
     538              :     /* Unknown chat variant. */
     539            2 :     TlWriter w; tl_writer_init(&w);
     540            2 :     tl_write_uint32(&w, CRC_future_chatPsychic);
     541            2 :     tl_write_int64 (&w, 1LL);
     542            2 :     TlReader r = tl_reader_init(w.data, w.len);
     543            2 :     ASSERT(tl_skip_chat(&r) == -1, "unknown chat CRC rejected");
     544            2 :     tl_writer_free(&w);
     545              : 
     546              :     /* Unknown user variant. */
     547            2 :     tl_writer_init(&w);
     548            2 :     tl_write_uint32(&w, CRC_future_userTimelord);
     549            2 :     tl_write_int64 (&w, 1LL);
     550            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     551            2 :     ASSERT(tl_skip_user(&r2) == -1, "unknown user CRC rejected");
     552            2 :     tl_writer_free(&w);
     553              : 
     554              :     /* chatEmpty happy path. */
     555            2 :     tl_writer_init(&w);
     556            2 :     tl_write_uint32(&w, TL_chatEmpty);
     557            2 :     tl_write_int64 (&w, 42LL);
     558            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     559            2 :     ChatSummary cs = {0};
     560            2 :     ASSERT(tl_extract_chat(&r3, &cs) == 0, "chatEmpty extracted");
     561            2 :     ASSERT(cs.id == 42LL, "chatEmpty id captured");
     562            2 :     ASSERT(cs.title[0] == '\0', "chatEmpty title blank");
     563            2 :     tl_writer_free(&w);
     564              : 
     565              :     /* userEmpty happy path. */
     566            2 :     tl_writer_init(&w);
     567            2 :     tl_write_uint32(&w, TL_userEmpty);
     568            2 :     tl_write_int64 (&w, 99LL);
     569            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     570            2 :     UserSummary us = {0};
     571            2 :     ASSERT(tl_extract_user(&r4, &us) == 0, "userEmpty extracted");
     572            2 :     ASSERT(us.id == 99LL, "userEmpty id captured");
     573            2 :     tl_writer_free(&w);
     574              : 
     575              :     /* Full user with first/last name + username + phone + access_hash —
     576              :      * drives many of the flag branches in extract_user_inner. */
     577            2 :     tl_writer_init(&w);
     578            2 :     tl_write_uint32(&w, TL_user);
     579              :     /* flags: 0=access_hash, 1=first_name, 2=last_name, 3=username, 4=phone */
     580            2 :     uint32_t uflags = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) | (1u << 4);
     581            2 :     tl_write_uint32(&w, uflags);
     582            2 :     tl_write_uint32(&w, 0);                     /* flags2 */
     583            2 :     tl_write_int64 (&w, 7001LL);
     584            2 :     tl_write_int64 (&w, 0xAABBCCDDEEFF0011LL);  /* access_hash */
     585            2 :     tl_write_string(&w, "Alice");
     586            2 :     tl_write_string(&w, "Wonder");
     587            2 :     tl_write_string(&w, "alice_wonder");
     588            2 :     tl_write_string(&w, "+10000000000");
     589            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
     590            2 :     UserSummary us5 = {0};
     591            2 :     ASSERT(tl_extract_user(&r5, &us5) == 0, "full user extracted");
     592            2 :     ASSERT(us5.id == 7001LL, "full user id");
     593            2 :     ASSERT(us5.have_access_hash == 1, "full user access_hash present");
     594            2 :     ASSERT(strcmp(us5.name, "Alice Wonder") == 0, "name joined");
     595            2 :     ASSERT(strcmp(us5.username, "alice_wonder") == 0, "username captured");
     596            2 :     tl_writer_free(&w);
     597              : }
     598              : 
     599              : /* Truncation — short buffers must fail cleanly instead of reading OOB. */
     600            2 : static void test_truncation_rejected(void) {
     601              :     /* Just the message CRC, nothing else. */
     602              :     uint8_t only_crc[4];
     603            2 :     only_crc[0] = 0x42; only_crc[1] = 0x52; only_crc[2] = 0x34; only_crc[3] = 0x94;
     604            2 :     TlReader r = tl_reader_init(only_crc, sizeof(only_crc));
     605            2 :     ASSERT(tl_skip_message(&r) == -1,
     606              :            "tl_skip_message rejects payload shorter than header");
     607              : 
     608              :     /* Zero-length buffer. */
     609            2 :     TlReader r0 = tl_reader_init(NULL, 0);
     610            2 :     ASSERT(tl_skip_message(&r0) == -1, "empty buffer rejected");
     611            2 :     ASSERT(tl_skip_peer(&r0) == -1,    "peer short read rejected");
     612            2 :     ASSERT(tl_skip_string(&r0) == -1,  "string short read rejected");
     613            2 :     ASSERT(tl_skip_bool(&r0) == -1,    "bool short read rejected");
     614              : }
     615              : 
     616              : /* ---------------------------------------------------------------- */
     617              : /* Extra surface coverage — the tl_skip.c file covers dozens of     */
     618              : /* nested TL types. Exercising the known-CRC branches of each one   */
     619              : /* matters for forward-compat because it proves that "unknown       */
     620              : /* returns -1, known returns 0" is a uniform contract, not a special */
     621              : /* case of the Message top-level only.                              */
     622              : /* ---------------------------------------------------------------- */
     623              : 
     624              : /* ---- PhotoSize + Photo ---- */
     625            2 : static void test_photo_and_photo_size_roundtrip(void) {
     626              :     /* Photo with flags.0, id + access_hash + file_reference + date + sizes +
     627              :      * dc_id. Walks photo_full, walk_photo_size_vector, tl_skip_photo_size. */
     628            2 :     TlWriter w; tl_writer_init(&w);
     629            2 :     tl_write_uint32(&w, 0xfb197a65u);            /* photo */
     630            2 :     tl_write_uint32(&w, 0);                      /* flags (no has_stickers,
     631              :                                                    no video_sizes) */
     632            2 :     tl_write_int64 (&w, 1001LL);                 /* id */
     633            2 :     tl_write_int64 (&w, 0xABCDEF0123456789LL);   /* access_hash */
     634              :     /* file_reference:bytes — empty is fine */
     635            2 :     tl_write_bytes (&w, (const unsigned char *)"", 0);
     636            2 :     tl_write_int32 (&w, 1700000000);             /* date */
     637              :     /* sizes:Vector<PhotoSize> — one photoSize variant */
     638            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 3);
     639              :     /* photoSize: type + w + h + size */
     640            2 :     tl_write_uint32(&w, 0x75c78e60u);
     641            2 :     tl_write_string(&w, "y");
     642            2 :     tl_write_int32 (&w, 1280); tl_write_int32(&w, 720); tl_write_int32(&w, 55555);
     643              :     /* photoCachedSize: type + w + h + bytes */
     644            2 :     tl_write_uint32(&w, 0x021e1ad6u);
     645            2 :     tl_write_string(&w, "s");
     646            2 :     tl_write_int32 (&w, 90); tl_write_int32(&w, 90);
     647            2 :     tl_write_bytes (&w, (const unsigned char *)"\x00\x01\x02", 3);
     648              :     /* photoSizeProgressive: type + w + h + Vector<int> */
     649            2 :     tl_write_uint32(&w, 0xfa3efb95u);
     650            2 :     tl_write_string(&w, "p");
     651            2 :     tl_write_int32 (&w, 1080); tl_write_int32(&w, 1920);
     652            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
     653            2 :     tl_write_int32 (&w, 100); tl_write_int32(&w, 200);
     654              :     /* dc_id */
     655            2 :     tl_write_int32 (&w, 2);
     656            2 :     TlReader r = tl_reader_init(w.data, w.len);
     657            2 :     ASSERT(tl_skip_photo(&r) == 0, "photo walked");
     658            2 :     ASSERT(r.pos == r.len, "reader consumed photo");
     659            2 :     tl_writer_free(&w);
     660              : 
     661              :     /* photoEmpty */
     662            2 :     tl_writer_init(&w);
     663            2 :     tl_write_uint32(&w, 0x2331b22du);
     664            2 :     tl_write_int64 (&w, 7777LL);
     665            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     666            2 :     ASSERT(tl_skip_photo(&r2) == 0, "photoEmpty walked");
     667            2 :     tl_writer_free(&w);
     668              : 
     669              :     /* photoSize variants individually */
     670            2 :     tl_writer_init(&w);
     671              :     /* photoSizeEmpty */
     672            2 :     tl_write_uint32(&w, 0x0e17e23cu);
     673            2 :     tl_write_string(&w, "x");
     674            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     675            2 :     ASSERT(tl_skip_photo_size(&r3) == 0, "photoSizeEmpty ok");
     676            2 :     tl_writer_free(&w);
     677              : 
     678              :     /* photoStrippedSize */
     679            2 :     tl_writer_init(&w);
     680            2 :     tl_write_uint32(&w, 0xe0b0bc2eu);
     681            2 :     tl_write_string(&w, "i");
     682            2 :     tl_write_bytes (&w, (const unsigned char *)"\xFF\xFE\xFD", 3);
     683            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     684            2 :     ASSERT(tl_skip_photo_size(&r4) == 0, "photoStrippedSize ok");
     685            2 :     tl_writer_free(&w);
     686              : 
     687              :     /* photoPathSize */
     688            2 :     tl_writer_init(&w);
     689            2 :     tl_write_uint32(&w, 0xd8214d41u);
     690            2 :     tl_write_string(&w, "j");
     691            2 :     tl_write_bytes (&w, (const unsigned char *)"abc", 3);
     692            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
     693            2 :     ASSERT(tl_skip_photo_size(&r5) == 0, "photoPathSize ok");
     694            2 :     tl_writer_free(&w);
     695              : 
     696              :     /* Unknown photoSize CRC */
     697            2 :     tl_writer_init(&w);
     698            2 :     tl_write_uint32(&w, 0xFF00BBCCu);
     699            2 :     TlReader r6 = tl_reader_init(w.data, w.len);
     700            2 :     ASSERT(tl_skip_photo_size(&r6) == -1, "unknown photoSize rejected");
     701            2 :     tl_writer_free(&w);
     702              : 
     703              :     /* photo_size_vector */
     704            2 :     tl_writer_init(&w);
     705            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
     706            2 :     tl_write_uint32(&w, 0x75c78e60u);
     707            2 :     tl_write_string(&w, "m");
     708            2 :     tl_write_int32 (&w, 320); tl_write_int32(&w, 240); tl_write_int32(&w, 1000);
     709            2 :     TlReader r7 = tl_reader_init(w.data, w.len);
     710            2 :     ASSERT(tl_skip_photo_size_vector(&r7) == 0, "vector walked");
     711            2 :     tl_writer_free(&w);
     712              : }
     713              : 
     714              : /* ---- Document with attributes ---- */
     715            2 : static void test_document_with_attributes(void) {
     716            2 :     TlWriter w; tl_writer_init(&w);
     717            2 :     tl_write_uint32(&w, 0x8fd4c4d8u);            /* document */
     718            2 :     tl_write_uint32(&w, 0);                      /* flags (no thumbs / video) */
     719            2 :     tl_write_int64 (&w, 1234LL);                 /* id */
     720            2 :     tl_write_int64 (&w, 0xFEEDFACECAFED00DLL);   /* access_hash */
     721            2 :     tl_write_bytes (&w, (const unsigned char *)"ref", 3);   /* file_reference */
     722            2 :     tl_write_int32 (&w, 1700000000);             /* date */
     723            2 :     tl_write_string(&w, "image/png");            /* mime_type */
     724            2 :     tl_write_int64 (&w, 4096LL);                 /* size */
     725            2 :     tl_write_int32 (&w, 2);                      /* dc_id */
     726              :     /* attributes: Vector<DocumentAttribute> — cover many variants */
     727            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 5);
     728              :     /* imageSize */
     729            2 :     tl_write_uint32(&w, 0x6c37c15cu);
     730            2 :     tl_write_int32 (&w, 800); tl_write_int32(&w, 600);
     731              :     /* animated */
     732            2 :     tl_write_uint32(&w, 0x11b58939u);
     733              :     /* filename */
     734            2 :     tl_write_uint32(&w, 0x15590068u);
     735            2 :     tl_write_string(&w, "selfie.png");
     736              :     /* audio flags=0, duration only */
     737            2 :     tl_write_uint32(&w, 0x9852f9c6u);
     738            2 :     tl_write_uint32(&w, 0);                      /* flags */
     739            2 :     tl_write_int32 (&w, 30);                     /* duration */
     740              :     /* hasStickers */
     741            2 :     tl_write_uint32(&w, 0x9801d2f7u);
     742            2 :     TlReader r = tl_reader_init(w.data, w.len);
     743            2 :     ASSERT(tl_skip_document(&r) == 0, "document walked");
     744            2 :     ASSERT(r.pos == r.len, "reader consumed document");
     745            2 :     tl_writer_free(&w);
     746              : 
     747              :     /* documentEmpty */
     748            2 :     tl_writer_init(&w);
     749            2 :     tl_write_uint32(&w, 0x36f8c871u);
     750            2 :     tl_write_int64 (&w, 42LL);
     751            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     752            2 :     ASSERT(tl_skip_document(&r2) == 0, "documentEmpty walked");
     753            2 :     tl_writer_free(&w);
     754              : }
     755              : 
     756              : /* ---- Message forward header ---- */
     757            2 : static void test_fwd_header_variants(void) {
     758              :     /* messageFwdHeader: flags with from_id (bit 0) + date only (simplest). */
     759            2 :     TlWriter w; tl_writer_init(&w);
     760            2 :     tl_write_uint32(&w, 0x4e4df4bbu);
     761            2 :     tl_write_uint32(&w, (1u << 0));              /* flags: from_id */
     762            2 :     tl_write_uint32(&w, TL_peerUser);
     763            2 :     tl_write_int64 (&w, 42LL);                   /* from_id peer */
     764            2 :     tl_write_int32 (&w, 1700001000);             /* date */
     765            2 :     TlReader r = tl_reader_init(w.data, w.len);
     766            2 :     ASSERT(tl_skip_message_fwd_header(&r) == 0, "fwd header walked");
     767            2 :     ASSERT(r.pos == r.len, "fwd reader consumed");
     768            2 :     tl_writer_free(&w);
     769              : 
     770              :     /* Unknown CRC */
     771            2 :     tl_writer_init(&w);
     772            2 :     tl_write_uint32(&w, 0xFF00FF77u);
     773            2 :     tl_write_uint32(&w, 0);
     774            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     775            2 :     ASSERT(tl_skip_message_fwd_header(&r2) == -1,
     776              :            "unknown fwd header CRC rejected");
     777            2 :     tl_writer_free(&w);
     778              : 
     779              :     /* Fuller fwd header — from_id + from_name + channel_post + post_author */
     780            2 :     tl_writer_init(&w);
     781            2 :     tl_write_uint32(&w, 0x4e4df4bbu);
     782            2 :     uint32_t ff = (1u << 0) | (1u << 2) | (1u << 3) | (1u << 5);
     783            2 :     tl_write_uint32(&w, ff);
     784            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 1000LL);
     785            2 :     tl_write_string(&w, "Anonymous");            /* from_name (flags.5) */
     786            2 :     tl_write_int32 (&w, 1700001000);             /* date */
     787            2 :     tl_write_int32 (&w, 55);                     /* channel_post (flags.2) */
     788            2 :     tl_write_string(&w, "Bot Author");           /* post_author (flags.3) */
     789            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     790            2 :     ASSERT(tl_skip_message_fwd_header(&r3) == 0,
     791              :            "fuller fwd header walked");
     792            2 :     tl_writer_free(&w);
     793              : }
     794              : 
     795              : /* ---- Reply header ---- */
     796            2 : static void test_reply_header_variants(void) {
     797              :     /* messageReplyHeader with reply_to_msg_id (flags.4) */
     798            2 :     TlWriter w; tl_writer_init(&w);
     799            2 :     tl_write_uint32(&w, 0xafbc09dbu);
     800            2 :     tl_write_uint32(&w, (1u << 4));
     801            2 :     tl_write_int32 (&w, 3000);                   /* reply_to_msg_id */
     802            2 :     TlReader r = tl_reader_init(w.data, w.len);
     803            2 :     ASSERT(tl_skip_message_reply_header(&r) == 0, "reply header walked");
     804            2 :     tl_writer_free(&w);
     805              : 
     806              :     /* messageReplyStoryHeader */
     807            2 :     tl_writer_init(&w);
     808            2 :     tl_write_uint32(&w, 0xe5af939u);
     809            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 42LL);
     810            2 :     tl_write_int32 (&w, 77);
     811            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     812            2 :     ASSERT(tl_skip_message_reply_header(&r2) == 0, "story header walked");
     813            2 :     tl_writer_free(&w);
     814              : 
     815              :     /* Unknown reply header CRC */
     816            2 :     tl_writer_init(&w);
     817            2 :     tl_write_uint32(&w, 0xFF00FF88u);
     818            2 :     tl_write_uint32(&w, 0);
     819            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     820            2 :     ASSERT(tl_skip_message_reply_header(&r3) == -1,
     821              :            "unknown reply header rejected");
     822            2 :     tl_writer_free(&w);
     823              : }
     824              : 
     825              : /* ---- Draft message ---- */
     826            2 : static void test_draft_message_variants(void) {
     827            2 :     TlWriter w; tl_writer_init(&w);
     828              :     /* draftMessageEmpty flags=0 */
     829            2 :     tl_write_uint32(&w, 0x1b0c841au);
     830            2 :     tl_write_uint32(&w, 0);
     831            2 :     TlReader r = tl_reader_init(w.data, w.len);
     832            2 :     ASSERT(tl_skip_draft_message(&r) == 0, "draftMessageEmpty walked");
     833            2 :     tl_writer_free(&w);
     834              : 
     835              :     /* draftMessageEmpty with date */
     836            2 :     tl_writer_init(&w);
     837            2 :     tl_write_uint32(&w, 0x1b0c841au);
     838            2 :     tl_write_uint32(&w, 1u);
     839            2 :     tl_write_int32 (&w, 1700000000);
     840            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     841            2 :     ASSERT(tl_skip_draft_message(&r2) == 0, "draftEmpty+date walked");
     842            2 :     tl_writer_free(&w);
     843              : 
     844              :     /* draftMessage non-empty: production chooses not to parse, so it
     845              :      * must return -1. */
     846            2 :     tl_writer_init(&w);
     847            2 :     tl_write_uint32(&w, 0x3fccf7efu);
     848            2 :     tl_write_uint32(&w, 0);
     849            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     850            2 :     ASSERT(tl_skip_draft_message(&r3) == -1, "non-empty draft rejected");
     851            2 :     tl_writer_free(&w);
     852              : 
     853              :     /* Unknown draft CRC */
     854            2 :     tl_writer_init(&w);
     855            2 :     tl_write_uint32(&w, 0xFF00FFAAu);
     856            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     857            2 :     ASSERT(tl_skip_draft_message(&r4) == -1, "unknown draft rejected");
     858            2 :     tl_writer_free(&w);
     859              : }
     860              : 
     861              : /* ---- Notification sound + peerNotifySettings ---- */
     862            2 : static void test_notification_sound_and_settings(void) {
     863              :     /* All four sound variants */
     864            2 :     TlWriter w; tl_writer_init(&w);
     865            2 :     tl_write_uint32(&w, 0x97e8bebeu);            /* default */
     866            2 :     TlReader r = tl_reader_init(w.data, w.len);
     867            2 :     ASSERT(tl_skip_notification_sound(&r) == 0, "default sound");
     868            2 :     tl_writer_free(&w);
     869              : 
     870            2 :     tl_writer_init(&w);
     871            2 :     tl_write_uint32(&w, 0x6f0c34dfu);            /* none */
     872            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     873            2 :     ASSERT(tl_skip_notification_sound(&r2) == 0, "none sound");
     874            2 :     tl_writer_free(&w);
     875              : 
     876            2 :     tl_writer_init(&w);
     877            2 :     tl_write_uint32(&w, 0x830b9ae4u);            /* local */
     878            2 :     tl_write_string(&w, "Chime");
     879            2 :     tl_write_string(&w, "chime.mp3");
     880            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     881            2 :     ASSERT(tl_skip_notification_sound(&r3) == 0, "local sound");
     882            2 :     tl_writer_free(&w);
     883              : 
     884            2 :     tl_writer_init(&w);
     885            2 :     tl_write_uint32(&w, 0xff6c8049u);            /* ringtone */
     886            2 :     tl_write_int64 (&w, 777LL);
     887            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     888            2 :     ASSERT(tl_skip_notification_sound(&r4) == 0, "ringtone sound");
     889            2 :     tl_writer_free(&w);
     890              : 
     891            2 :     tl_writer_init(&w);
     892            2 :     tl_write_uint32(&w, 0xFF00FF11u);            /* unknown */
     893            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
     894            2 :     ASSERT(tl_skip_notification_sound(&r5) == -1, "unknown sound rejected");
     895            2 :     tl_writer_free(&w);
     896              : 
     897              :     /* peerNotifySettings with many sub-fields. */
     898            2 :     tl_writer_init(&w);
     899            2 :     tl_write_uint32(&w, 0xa83b0426u);
     900              :     /* flags: show_previews(0), silent(1), mute_until(2), ios(3), android(4),
     901              :      *        other(5), stories_muted(6), stories_hide_sender(7) */
     902            2 :     uint32_t sflags = (1u << 0) | (1u << 1) | (1u << 2) |
     903              :                       (1u << 3) | (1u << 4) | (1u << 5) |
     904              :                       (1u << 6) | (1u << 7);
     905            2 :     tl_write_uint32(&w, sflags);
     906            2 :     tl_write_uint32(&w, TL_boolTrue);            /* show_previews */
     907            2 :     tl_write_uint32(&w, TL_boolFalse);           /* silent */
     908            2 :     tl_write_int32 (&w, 1700100000);             /* mute_until */
     909            2 :     tl_write_uint32(&w, 0x97e8bebeu);            /* ios_sound default */
     910            2 :     tl_write_uint32(&w, 0x6f0c34dfu);            /* android none */
     911            2 :     tl_write_uint32(&w, 0x97e8bebeu);            /* other default */
     912            2 :     tl_write_uint32(&w, TL_boolTrue);            /* stories_muted */
     913            2 :     tl_write_uint32(&w, TL_boolFalse);           /* stories_hide_sender */
     914            2 :     TlReader rs = tl_reader_init(w.data, w.len);
     915            2 :     ASSERT(tl_skip_peer_notify_settings(&rs) == 0, "settings walked");
     916            2 :     ASSERT(rs.pos == rs.len, "settings reader consumed");
     917            2 :     tl_writer_free(&w);
     918              : 
     919              :     /* Unknown settings CRC */
     920            2 :     tl_writer_init(&w);
     921            2 :     tl_write_uint32(&w, 0xFF00FF22u);
     922            2 :     tl_write_uint32(&w, 0);
     923            2 :     TlReader ru = tl_reader_init(w.data, w.len);
     924            2 :     ASSERT(tl_skip_peer_notify_settings(&ru) == -1,
     925              :            "unknown settings rejected");
     926            2 :     tl_writer_free(&w);
     927              : }
     928              : 
     929              : /* ---- Chat photo + user profile photo + user status ---- */
     930            2 : static void test_chat_user_visuals(void) {
     931              :     /* chatPhotoEmpty / chatPhoto */
     932            2 :     TlWriter w; tl_writer_init(&w);
     933            2 :     tl_write_uint32(&w, 0x37c1011cu);
     934            2 :     TlReader r = tl_reader_init(w.data, w.len);
     935            2 :     ASSERT(tl_skip_chat_photo(&r) == 0, "chatPhotoEmpty");
     936            2 :     tl_writer_free(&w);
     937              : 
     938            2 :     tl_writer_init(&w);
     939            2 :     tl_write_uint32(&w, 0x1c6e1c11u);
     940            2 :     tl_write_uint32(&w, 0);                      /* flags */
     941            2 :     tl_write_int64 (&w, 5001LL);                 /* photo_id */
     942            2 :     tl_write_int32 (&w, 2);                      /* dc_id */
     943            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
     944            2 :     ASSERT(tl_skip_chat_photo(&r2) == 0, "chatPhoto");
     945            2 :     tl_writer_free(&w);
     946              : 
     947              :     /* chatPhoto with stripped_thumb (flags.1) */
     948            2 :     tl_writer_init(&w);
     949            2 :     tl_write_uint32(&w, 0x1c6e1c11u);
     950            2 :     tl_write_uint32(&w, (1u << 1));
     951            2 :     tl_write_int64 (&w, 5002LL);
     952            2 :     tl_write_bytes (&w, (const unsigned char *)"stripped", 8);
     953            2 :     tl_write_int32 (&w, 4);
     954            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
     955            2 :     ASSERT(tl_skip_chat_photo(&r3) == 0, "chatPhoto+stripped");
     956            2 :     tl_writer_free(&w);
     957              : 
     958              :     /* Unknown chatPhoto */
     959            2 :     tl_writer_init(&w);
     960            2 :     tl_write_uint32(&w, 0xFF00FF33u);
     961            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
     962            2 :     ASSERT(tl_skip_chat_photo(&r4) == -1, "unknown chatPhoto rejected");
     963            2 :     tl_writer_free(&w);
     964              : 
     965              :     /* userProfilePhotoEmpty / userProfilePhoto */
     966            2 :     tl_writer_init(&w);
     967            2 :     tl_write_uint32(&w, 0x4f11bae1u);
     968            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
     969            2 :     ASSERT(tl_skip_user_profile_photo(&r5) == 0, "userProfilePhotoEmpty");
     970            2 :     tl_writer_free(&w);
     971              : 
     972            2 :     tl_writer_init(&w);
     973            2 :     tl_write_uint32(&w, 0x82d1f706u);
     974            2 :     tl_write_uint32(&w, 0);
     975            2 :     tl_write_int64 (&w, 9000LL);
     976            2 :     tl_write_int32 (&w, 5);
     977            2 :     TlReader r6 = tl_reader_init(w.data, w.len);
     978            2 :     ASSERT(tl_skip_user_profile_photo(&r6) == 0, "userProfilePhoto");
     979            2 :     tl_writer_free(&w);
     980              : 
     981              :     /* Unknown userProfilePhoto */
     982            2 :     tl_writer_init(&w);
     983            2 :     tl_write_uint32(&w, 0xFF00FF44u);
     984            2 :     TlReader r7 = tl_reader_init(w.data, w.len);
     985            2 :     ASSERT(tl_skip_user_profile_photo(&r7) == -1, "unknown rejected");
     986            2 :     tl_writer_free(&w);
     987              : 
     988              :     /* userStatusEmpty carries no payload; the other three (recently,
     989              :      * lastWeek, lastMonth) each read an int32 per the 170+ schema. */
     990            2 :     tl_writer_init(&w);
     991            2 :     tl_write_uint32(&w, 0x09d05049u);            /* empty */
     992            2 :     TlReader rse = tl_reader_init(w.data, w.len);
     993            2 :     ASSERT(tl_skip_user_status(&rse) == 0, "empty user status");
     994            2 :     tl_writer_free(&w);
     995              : 
     996            2 :     uint32_t statuses_with_int[] = {
     997              :         0x7b197dc8u, 0x541a1d1au, 0x65899e67u
     998              :     };
     999            8 :     for (size_t i = 0; i < sizeof(statuses_with_int)/sizeof(statuses_with_int[0]); i++) {
    1000            6 :         tl_writer_init(&w);
    1001            6 :         tl_write_uint32(&w, statuses_with_int[i]);
    1002            6 :         tl_write_int32 (&w, 1700000000);
    1003            6 :         TlReader rr = tl_reader_init(w.data, w.len);
    1004            6 :         ASSERT(tl_skip_user_status(&rr) == 0, "recently/lastWeek/lastMonth status");
    1005            6 :         tl_writer_free(&w);
    1006              :     }
    1007              : 
    1008              :     /* Online/Offline carry an int32 expires/was_online. */
    1009            2 :     tl_writer_init(&w);
    1010            2 :     tl_write_uint32(&w, 0xedb93949u);
    1011            2 :     tl_write_int32 (&w, 1700200000);
    1012            2 :     TlReader ro = tl_reader_init(w.data, w.len);
    1013            2 :     ASSERT(tl_skip_user_status(&ro) == 0, "online status");
    1014            2 :     tl_writer_free(&w);
    1015              : 
    1016            2 :     tl_writer_init(&w);
    1017            2 :     tl_write_uint32(&w, 0x008c703fu);
    1018            2 :     tl_write_int32 (&w, 1700100000);
    1019            2 :     TlReader roff = tl_reader_init(w.data, w.len);
    1020            2 :     ASSERT(tl_skip_user_status(&roff) == 0, "offline status");
    1021            2 :     tl_writer_free(&w);
    1022              : 
    1023              :     /* Unknown status */
    1024            2 :     tl_writer_init(&w);
    1025            2 :     tl_write_uint32(&w, 0xFF00FF55u);
    1026            2 :     TlReader ru2 = tl_reader_init(w.data, w.len);
    1027            2 :     ASSERT(tl_skip_user_status(&ru2) == -1, "unknown status rejected");
    1028            2 :     tl_writer_free(&w);
    1029              : }
    1030              : 
    1031              : /* ---- Username vector + peer color + emoji status ---- */
    1032            2 : static void test_username_color_emoji(void) {
    1033              :     /* Vector<Username> of length 2 */
    1034            2 :     TlWriter w; tl_writer_init(&w);
    1035            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1036            2 :     tl_write_uint32(&w, 0xb4073647u);            /* username */
    1037            2 :     tl_write_uint32(&w, 0);                      /* flags */
    1038            2 :     tl_write_string(&w, "alice");
    1039            2 :     tl_write_uint32(&w, 0xb4073647u);
    1040            2 :     tl_write_uint32(&w, 0);
    1041            2 :     tl_write_string(&w, "bob");
    1042            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1043            2 :     ASSERT(tl_skip_username_vector(&r) == 0, "username vector walked");
    1044            2 :     ASSERT(r.pos == r.len, "reader consumed");
    1045            2 :     tl_writer_free(&w);
    1046              : 
    1047              :     /* Unknown entry in username vector. */
    1048            2 :     tl_writer_init(&w);
    1049            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1050            2 :     tl_write_uint32(&w, 0xFF00FF66u);
    1051            2 :     tl_write_uint32(&w, 0);
    1052            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1053            2 :     ASSERT(tl_skip_username_vector(&r2) == -1,
    1054              :            "unknown username entry rejected");
    1055            2 :     tl_writer_free(&w);
    1056              : 
    1057              :     /* peerColor with color + emoji_id */
    1058            2 :     tl_writer_init(&w);
    1059            2 :     tl_write_uint32(&w, 0xb54b5acfu);
    1060            2 :     tl_write_uint32(&w, (1u << 0) | (1u << 1));
    1061            2 :     tl_write_int32 (&w, 5);
    1062            2 :     tl_write_int64 (&w, 500001LL);
    1063            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1064            2 :     ASSERT(tl_skip_peer_color(&r3) == 0, "peerColor walked");
    1065            2 :     tl_writer_free(&w);
    1066              : 
    1067              :     /* Unknown peerColor CRC */
    1068            2 :     tl_writer_init(&w);
    1069            2 :     tl_write_uint32(&w, 0xFF00FF77u);
    1070            2 :     tl_write_uint32(&w, 0);
    1071            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
    1072            2 :     ASSERT(tl_skip_peer_color(&r4) == -1, "unknown peerColor rejected");
    1073            2 :     tl_writer_free(&w);
    1074              : 
    1075              :     /* EmojiStatus all three */
    1076            2 :     tl_writer_init(&w);
    1077            2 :     tl_write_uint32(&w, 0x2de11aaeu);            /* empty */
    1078            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
    1079            2 :     ASSERT(tl_skip_emoji_status(&r5) == 0, "emojiStatusEmpty");
    1080            2 :     tl_writer_free(&w);
    1081              : 
    1082            2 :     tl_writer_init(&w);
    1083            2 :     tl_write_uint32(&w, 0x929b619du);
    1084            2 :     tl_write_int64 (&w, 123LL);
    1085            2 :     TlReader r6 = tl_reader_init(w.data, w.len);
    1086            2 :     ASSERT(tl_skip_emoji_status(&r6) == 0, "emojiStatus");
    1087            2 :     tl_writer_free(&w);
    1088              : 
    1089            2 :     tl_writer_init(&w);
    1090            2 :     tl_write_uint32(&w, 0xfa30a8c7u);
    1091            2 :     tl_write_int64 (&w, 456LL);
    1092            2 :     tl_write_int32 (&w, 1700500000);
    1093            2 :     TlReader r7 = tl_reader_init(w.data, w.len);
    1094            2 :     ASSERT(tl_skip_emoji_status(&r7) == 0, "emojiStatusUntil");
    1095            2 :     tl_writer_free(&w);
    1096              : 
    1097              :     /* Unknown emoji status */
    1098            2 :     tl_writer_init(&w);
    1099            2 :     tl_write_uint32(&w, 0xFF00FF88u);
    1100            2 :     TlReader r8 = tl_reader_init(w.data, w.len);
    1101            2 :     ASSERT(tl_skip_emoji_status(&r8) == -1, "unknown emojiStatus rejected");
    1102            2 :     tl_writer_free(&w);
    1103              : }
    1104              : 
    1105              : /* ---- Restriction reason vector ---- */
    1106            2 : static void test_restriction_reason_vector(void) {
    1107            2 :     TlWriter w; tl_writer_init(&w);
    1108            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1109            2 :     tl_write_uint32(&w, 0xd072acb4u);
    1110            2 :     tl_write_string(&w, "android");
    1111            2 :     tl_write_string(&w, "sensitive");
    1112            2 :     tl_write_string(&w, "Restricted");
    1113            2 :     tl_write_uint32(&w, 0xd072acb4u);
    1114            2 :     tl_write_string(&w, "ios");
    1115            2 :     tl_write_string(&w, "porn");
    1116            2 :     tl_write_string(&w, "NSFW");
    1117            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1118            2 :     ASSERT(tl_skip_restriction_reason_vector(&r) == 0,
    1119              :            "restriction reason vector walked");
    1120            2 :     tl_writer_free(&w);
    1121              : 
    1122              :     /* Unknown inner CRC */
    1123            2 :     tl_writer_init(&w);
    1124            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1125            2 :     tl_write_uint32(&w, 0xFF00FF99u);
    1126            2 :     tl_write_string(&w, "x"); tl_write_string(&w, "y"); tl_write_string(&w, "z");
    1127            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1128            2 :     ASSERT(tl_skip_restriction_reason_vector(&r2) == -1,
    1129              :            "unknown restriction reason rejected");
    1130            2 :     tl_writer_free(&w);
    1131              : }
    1132              : 
    1133              : /* ---- Factcheck ---- */
    1134            2 : static void test_factcheck_variants(void) {
    1135              :     /* factCheck with country + text + hash. */
    1136            2 :     TlWriter w; tl_writer_init(&w);
    1137            2 :     tl_write_uint32(&w, 0xb89bfccfu);
    1138            2 :     tl_write_uint32(&w, (1u << 1));
    1139            2 :     tl_write_string(&w, "HU");
    1140            2 :     tl_write_uint32(&w, 0x751f3146u);            /* textWithEntities */
    1141            2 :     tl_write_string(&w, "fact-checked");
    1142            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1143            2 :     tl_write_int64 (&w, 0xDEADBEEFCAFEBABELL);   /* hash */
    1144            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1145            2 :     ASSERT(tl_skip_factcheck(&r) == 0, "factcheck walked");
    1146            2 :     tl_writer_free(&w);
    1147              : 
    1148              :     /* factCheck with no flags set (just hash). */
    1149            2 :     tl_writer_init(&w);
    1150            2 :     tl_write_uint32(&w, 0xb89bfccfu);
    1151            2 :     tl_write_uint32(&w, 0);
    1152            2 :     tl_write_int64 (&w, 77LL);
    1153            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1154            2 :     ASSERT(tl_skip_factcheck(&r2) == 0, "flagless factcheck");
    1155            2 :     tl_writer_free(&w);
    1156              : 
    1157              :     /* Unknown factcheck CRC */
    1158            2 :     tl_writer_init(&w);
    1159            2 :     tl_write_uint32(&w, 0xFF00FFAAu);
    1160            2 :     tl_write_uint32(&w, 0);
    1161            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1162            2 :     ASSERT(tl_skip_factcheck(&r3) == -1, "unknown factcheck rejected");
    1163            2 :     tl_writer_free(&w);
    1164              : }
    1165              : 
    1166              : /* ---- MessageReactions + MessageReplies ---- */
    1167            2 : static void test_reactions_replies_trailers(void) {
    1168              :     /* MessageReactions with two ReactionCount entries (emoji + custom emoji). */
    1169            2 :     TlWriter w; tl_writer_init(&w);
    1170            2 :     tl_write_uint32(&w, 0x4f2b9479u);            /* messageReactions */
    1171            2 :     tl_write_uint32(&w, 0);                      /* flags */
    1172            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1173              :     /* reactionCount: flags=0, reaction = reactionEmoji, count=5 */
    1174            2 :     tl_write_uint32(&w, 0xa3d1cb80u);
    1175            2 :     tl_write_uint32(&w, 0);
    1176            2 :     tl_write_uint32(&w, 0x1b2286b8u);
    1177            2 :     tl_write_string(&w, "\xf0\x9f\x94\xa5");
    1178            2 :     tl_write_int32 (&w, 5);
    1179              :     /* reactionCount with chosen_order flag + reactionCustomEmoji */
    1180            2 :     tl_write_uint32(&w, 0xa3d1cb80u);
    1181            2 :     tl_write_uint32(&w, (1u << 0));
    1182            2 :     tl_write_int32 (&w, 1);
    1183            2 :     tl_write_uint32(&w, 0x8935fc73u);
    1184            2 :     tl_write_int64 (&w, 424242LL);
    1185            2 :     tl_write_int32 (&w, 2);
    1186            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1187            2 :     ASSERT(tl_skip_message_reactions(&r) == 0, "reactions walked");
    1188            2 :     tl_writer_free(&w);
    1189              : 
    1190              :     /* Reactions with unknown inner Reaction CRC. */
    1191            2 :     tl_writer_init(&w);
    1192            2 :     tl_write_uint32(&w, 0x4f2b9479u);
    1193            2 :     tl_write_uint32(&w, 0);
    1194            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1195            2 :     tl_write_uint32(&w, 0xa3d1cb80u);
    1196            2 :     tl_write_uint32(&w, 0);
    1197            2 :     tl_write_uint32(&w, 0xFF00FFBBu);            /* unknown reaction */
    1198            2 :     tl_write_int32 (&w, 1);
    1199            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1200            2 :     ASSERT(tl_skip_message_reactions(&r2) == -1,
    1201              :            "unknown reaction inner CRC rejected");
    1202            2 :     tl_writer_free(&w);
    1203              : 
    1204              :     /* Reactions with recent_reactions (flags.1) — production bails. */
    1205            2 :     tl_writer_init(&w);
    1206            2 :     tl_write_uint32(&w, 0x4f2b9479u);
    1207            2 :     tl_write_uint32(&w, (1u << 1));              /* recent_reactions present */
    1208            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1209            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1210            2 :     ASSERT(tl_skip_message_reactions(&r3) == -1,
    1211              :            "recent_reactions bail");
    1212            2 :     tl_writer_free(&w);
    1213              : 
    1214              :     /* MessageReplies full: flags=0b1111 with all optionals. */
    1215            2 :     tl_writer_init(&w);
    1216            2 :     tl_write_uint32(&w, 0x83d60fc2u);
    1217            2 :     tl_write_uint32(&w, 0xF);                    /* all four bits */
    1218            2 :     tl_write_int32 (&w, 10);                     /* replies */
    1219            2 :     tl_write_int32 (&w, 100);                    /* replies_pts */
    1220              :     /* recent_repliers (flags.1): Vector<Peer> */
    1221            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1222            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 42LL);
    1223            2 :     tl_write_int64 (&w, 9999LL);                 /* channel_id (flags.0) */
    1224            2 :     tl_write_int32 (&w, 20);                     /* max_id (flags.2) */
    1225            2 :     tl_write_int32 (&w, 5);                      /* read_max_id (flags.3) */
    1226            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
    1227            2 :     ASSERT(tl_skip_message_replies(&r4) == 0, "replies walked");
    1228            2 :     tl_writer_free(&w);
    1229              : 
    1230              :     /* Unknown messageReplies CRC */
    1231            2 :     tl_writer_init(&w);
    1232            2 :     tl_write_uint32(&w, 0xFF00FFCCu);
    1233            2 :     tl_write_uint32(&w, 0);
    1234            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
    1235            2 :     ASSERT(tl_skip_message_replies(&r5) == -1, "unknown replies rejected");
    1236            2 :     tl_writer_free(&w);
    1237              : }
    1238              : 
    1239              : /* ---- Photo MessageMedia round-trip through tl_skip_message_media ---- */
    1240            2 : static void test_media_photo_and_document(void) {
    1241              :     /* messageMediaPhoto with a photo inner object. */
    1242            2 :     TlWriter w; tl_writer_init(&w);
    1243            2 :     tl_write_uint32(&w, 0x695150d7u);            /* messageMediaPhoto */
    1244            2 :     tl_write_uint32(&w, (1u << 0));              /* flags.0 → photo present */
    1245              :     /* photo_full */
    1246            2 :     tl_write_uint32(&w, 0xfb197a65u);
    1247            2 :     tl_write_uint32(&w, 0);
    1248            2 :     tl_write_int64 (&w, 1LL);
    1249            2 :     tl_write_int64 (&w, 2LL);
    1250            2 :     tl_write_bytes (&w, (const unsigned char *)"", 0);
    1251            2 :     tl_write_int32 (&w, 1700000000);
    1252            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1253            2 :     tl_write_uint32(&w, 0x75c78e60u);
    1254            2 :     tl_write_string(&w, "y");
    1255            2 :     tl_write_int32 (&w, 1280); tl_write_int32(&w, 720); tl_write_int32(&w, 12345);
    1256            2 :     tl_write_int32 (&w, 2);                      /* dc_id */
    1257            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1258            2 :     MediaInfo mi = {0};
    1259            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "media photo walked");
    1260            2 :     ASSERT(mi.kind == MEDIA_PHOTO, "kind=PHOTO");
    1261            2 :     ASSERT(mi.photo_id == 1LL, "photo_id captured");
    1262            2 :     tl_writer_free(&w);
    1263              : 
    1264              :     /* messageMediaDocument with an inner document. */
    1265            2 :     tl_writer_init(&w);
    1266            2 :     tl_write_uint32(&w, 0x4cf4d72du);
    1267            2 :     tl_write_uint32(&w, (1u << 0));              /* document present */
    1268            2 :     tl_write_uint32(&w, 0x8fd4c4d8u);            /* document */
    1269            2 :     tl_write_uint32(&w, 0);                      /* flags */
    1270            2 :     tl_write_int64 (&w, 111LL);                  /* id */
    1271            2 :     tl_write_int64 (&w, 222LL);                  /* access_hash */
    1272            2 :     tl_write_bytes (&w, (const unsigned char *)"r", 1);
    1273            2 :     tl_write_int32 (&w, 1700000000);             /* date */
    1274            2 :     tl_write_string(&w, "video/mp4");
    1275            2 :     tl_write_int64 (&w, 1024LL);                 /* size */
    1276            2 :     tl_write_int32 (&w, 4);                      /* dc_id */
    1277            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1278              :     /* documentAttributeFilename */
    1279            2 :     tl_write_uint32(&w, 0x15590068u);
    1280            2 :     tl_write_string(&w, "movie.mp4");
    1281            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1282            2 :     MediaInfo mi2 = {0};
    1283            2 :     ASSERT(tl_skip_message_media_ex(&r2, &mi2) == 0, "media document walked");
    1284            2 :     ASSERT(mi2.kind == MEDIA_DOCUMENT, "kind=DOCUMENT");
    1285            2 :     ASSERT(mi2.document_id == 111LL, "document_id captured");
    1286            2 :     ASSERT(strcmp(mi2.document_mime, "video/mp4") == 0, "mime captured");
    1287            2 :     ASSERT(strcmp(mi2.document_filename, "movie.mp4") == 0, "filename");
    1288            2 :     tl_writer_free(&w);
    1289              : }
    1290              : 
    1291              : /* ---- Empty MessageReplies path (flags=0) ---- */
    1292            2 : static void test_message_replies_empty(void) {
    1293            2 :     TlWriter w; tl_writer_init(&w);
    1294            2 :     tl_write_uint32(&w, 0x83d60fc2u);
    1295            2 :     tl_write_uint32(&w, 0);                      /* flags */
    1296            2 :     tl_write_int32 (&w, 0);                      /* replies */
    1297            2 :     tl_write_int32 (&w, 0);                      /* replies_pts */
    1298            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1299            2 :     ASSERT(tl_skip_message_replies(&r) == 0, "empty replies walked");
    1300            2 :     ASSERT(r.pos == r.len, "reader consumed");
    1301            2 :     tl_writer_free(&w);
    1302              : }
    1303              : 
    1304              : /* ---- messageMediaGame → skip_game ---- */
    1305            2 : static void test_media_game(void) {
    1306              :     /* messageMediaGame + game#bdf9653b flags=0 (no document). */
    1307            2 :     TlWriter w; tl_writer_init(&w);
    1308            2 :     tl_write_uint32(&w, 0xfdb19008u);          /* messageMediaGame */
    1309              :     /* game#bdf9653b flags:# id:long access_hash:long short_name title desc photo */
    1310            2 :     tl_write_uint32(&w, 0xbdf9653bu);
    1311            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1312            2 :     tl_write_int64 (&w, 5000LL);               /* id */
    1313            2 :     tl_write_int64 (&w, 6000LL);               /* access_hash */
    1314            2 :     tl_write_string(&w, "snake");              /* short_name */
    1315            2 :     tl_write_string(&w, "Snake");              /* title */
    1316            2 :     tl_write_string(&w, "Classic snake");      /* description */
    1317              :     /* photo: photoEmpty */
    1318            2 :     tl_write_uint32(&w, 0x2331b22du);
    1319            2 :     tl_write_int64 (&w, 0LL);
    1320            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1321            2 :     MediaInfo mi = {0};
    1322            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "mediaGame walked");
    1323            2 :     ASSERT(mi.kind == MEDIA_GAME, "kind=GAME");
    1324            2 :     ASSERT(r.pos == r.len, "reader consumed game");
    1325            2 :     tl_writer_free(&w);
    1326              : 
    1327              :     /* game with document (flags.0). */
    1328            2 :     tl_writer_init(&w);
    1329            2 :     tl_write_uint32(&w, 0xfdb19008u);
    1330            2 :     tl_write_uint32(&w, 0xbdf9653bu);
    1331            2 :     tl_write_uint32(&w, (1u << 0));            /* flags.0 → document present */
    1332            2 :     tl_write_int64 (&w, 7000LL);
    1333            2 :     tl_write_int64 (&w, 8000LL);
    1334            2 :     tl_write_string(&w, "pong");
    1335            2 :     tl_write_string(&w, "Pong");
    1336            2 :     tl_write_string(&w, "Old-school pong");
    1337            2 :     tl_write_uint32(&w, 0x2331b22du);          /* photoEmpty */
    1338            2 :     tl_write_int64 (&w, 0LL);
    1339              :     /* documentEmpty */
    1340            2 :     tl_write_uint32(&w, 0x36f8c871u);
    1341            2 :     tl_write_int64 (&w, 42LL);
    1342            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1343            2 :     ASSERT(tl_skip_message_media_ex(&r2, NULL) == 0, "game+doc walked");
    1344            2 :     ASSERT(r2.pos == r2.len, "reader consumed game+doc");
    1345            2 :     tl_writer_free(&w);
    1346              : 
    1347              :     /* unknown game CRC → -1 */
    1348            2 :     tl_writer_init(&w);
    1349            2 :     tl_write_uint32(&w, 0xfdb19008u);
    1350            2 :     tl_write_uint32(&w, 0xFF00ABCDu);
    1351            2 :     tl_write_uint32(&w, 0);
    1352            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1353            2 :     ASSERT(tl_skip_message_media_ex(&r3, NULL) == -1,
    1354              :            "unknown game CRC rejected");
    1355            2 :     tl_writer_free(&w);
    1356              : }
    1357              : 
    1358              : /* ---- messageMediaInvoice → skip_web_document + skip_message_extended_media ---- */
    1359            2 : static void test_media_invoice(void) {
    1360              :     /* Invoice with photo (webDocument, flags.0) — no receipt, no ext-media. */
    1361            2 :     TlWriter w; tl_writer_init(&w);
    1362            2 :     tl_write_uint32(&w, 0xf6a548d3u);          /* messageMediaInvoice */
    1363            2 :     tl_write_uint32(&w, (1u << 0));            /* flags.0 → photo present */
    1364            2 :     tl_write_string(&w, "Ticket");             /* title */
    1365            2 :     tl_write_string(&w, "One event entry");   /* description */
    1366              :     /* webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attrs:Vector */
    1367            2 :     tl_write_uint32(&w, 0xf9c8bcc6u);
    1368            2 :     tl_write_string(&w, "https://img.example.com/t.jpg");
    1369            2 :     tl_write_int32 (&w, 1024);                 /* size */
    1370            2 :     tl_write_string(&w, "image/jpeg");
    1371            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1372              :     /* currency + amount + start_param */
    1373            2 :     tl_write_string(&w, "USD");
    1374            2 :     tl_write_int64 (&w, 500LL);
    1375            2 :     tl_write_string(&w, "start123");
    1376            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1377            2 :     MediaInfo mi = {0};
    1378            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "invoice+webDoc walked");
    1379            2 :     ASSERT(mi.kind == MEDIA_INVOICE, "kind=INVOICE");
    1380            2 :     ASSERT(r.pos == r.len, "reader consumed invoice");
    1381            2 :     tl_writer_free(&w);
    1382              : 
    1383              :     /* Invoice with webDocument (full, has access_hash), no ext-media. */
    1384            2 :     tl_writer_init(&w);
    1385            2 :     tl_write_uint32(&w, 0xf6a548d3u);
    1386            2 :     tl_write_uint32(&w, (1u << 0));
    1387            2 :     tl_write_string(&w, "Prod");
    1388            2 :     tl_write_string(&w, "Desc");
    1389              :     /* webDocument#1c570ed1 url access_hash size mime attrs */
    1390            2 :     tl_write_uint32(&w, 0x1c570ed1u);
    1391            2 :     tl_write_string(&w, "https://cdn.tg/img.png");
    1392            2 :     tl_write_int64 (&w, 999LL);                /* access_hash */
    1393            2 :     tl_write_int32 (&w, 2048);
    1394            2 :     tl_write_string(&w, "image/png");
    1395            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1396            2 :     tl_write_uint32(&w, 0x11b58939u);          /* documentAttributeAnimated */
    1397            2 :     tl_write_string(&w, "EUR");
    1398            2 :     tl_write_int64 (&w, 1000LL);
    1399            2 :     tl_write_string(&w, "");
    1400            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1401            2 :     ASSERT(tl_skip_message_media_ex(&r2, NULL) == 0, "invoice+webDoc full");
    1402            2 :     ASSERT(r2.pos == r2.len, "consumed invoice full");
    1403            2 :     tl_writer_free(&w);
    1404              : 
    1405              :     /* Unknown webDocument CRC → -1 */
    1406            2 :     tl_writer_init(&w);
    1407            2 :     tl_write_uint32(&w, 0xf6a548d3u);
    1408            2 :     tl_write_uint32(&w, (1u << 0));
    1409            2 :     tl_write_string(&w, "T"); tl_write_string(&w, "D");
    1410            2 :     tl_write_uint32(&w, 0xFF00FEFEu);          /* unknown webDocument */
    1411            2 :     tl_write_string(&w, "USD"); tl_write_int64(&w, 0LL); tl_write_string(&w, "");
    1412            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1413            2 :     ASSERT(tl_skip_message_media_ex(&r3, NULL) == -1,
    1414              :            "unknown webDocument CRC rejected");
    1415            2 :     tl_writer_free(&w);
    1416              : }
    1417              : 
    1418              : /* ---- messageMediaPaidMedia → skip_message_extended_media ---- */
    1419            2 : static void test_media_paid_media(void) {
    1420              :     /* Paid media with one messageExtendedMediaPreview (flags=0). */
    1421            2 :     TlWriter w; tl_writer_init(&w);
    1422            2 :     tl_write_uint32(&w, 0xa8852491u);          /* messageMediaPaidMedia */
    1423            2 :     tl_write_int64 (&w, 25LL);                 /* stars_amount */
    1424            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1425              :     /* messageExtendedMediaPreview#ad628cc8 flags=0 (no w/h, no thumb, no dur) */
    1426            2 :     tl_write_uint32(&w, 0xad628cc8u);
    1427            2 :     tl_write_uint32(&w, 0);
    1428            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1429            2 :     MediaInfo mi = {0};
    1430            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "paidMedia preview walked");
    1431            2 :     ASSERT(mi.kind == MEDIA_PAID, "kind=PAID");
    1432            2 :     ASSERT(r.pos == r.len, "reader consumed paid preview");
    1433            2 :     tl_writer_free(&w);
    1434              : 
    1435              :     /* Paid media preview with all flags set (w+h + thumb + video_duration). */
    1436            2 :     tl_writer_init(&w);
    1437            2 :     tl_write_uint32(&w, 0xa8852491u);
    1438            2 :     tl_write_int64 (&w, 50LL);
    1439            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1440            2 :     tl_write_uint32(&w, 0xad628cc8u);
    1441            2 :     tl_write_uint32(&w, 0x7u);                 /* flags: bits 0,1,2 */
    1442            2 :     tl_write_int32 (&w, 640); tl_write_int32(&w, 480);
    1443              :     /* thumb: photoSizeEmpty */
    1444            2 :     tl_write_uint32(&w, 0x0e17e23cu);
    1445            2 :     tl_write_string(&w, "s");
    1446            2 :     tl_write_int32 (&w, 10);                   /* video_duration */
    1447            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1448            2 :     ASSERT(tl_skip_message_media_ex(&r2, NULL) == 0, "paid preview all-flags");
    1449            2 :     ASSERT(r2.pos == r2.len, "consumed paid preview all-flags");
    1450            2 :     tl_writer_free(&w);
    1451              : 
    1452              :     /* Paid with messageExtendedMedia (wraps another MessageMedia). */
    1453            2 :     tl_writer_init(&w);
    1454            2 :     tl_write_uint32(&w, 0xa8852491u);
    1455            2 :     tl_write_int64 (&w, 10LL);
    1456            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1457            2 :     tl_write_uint32(&w, 0xee479c64u);          /* messageExtendedMedia */
    1458              :     /* wrapped: messageMediaEmpty */
    1459            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1460            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1461            2 :     ASSERT(tl_skip_message_media_ex(&r3, NULL) == 0, "extMedia wrapper walked");
    1462            2 :     ASSERT(r3.pos == r3.len, "consumed extMedia");
    1463            2 :     tl_writer_free(&w);
    1464              : 
    1465              :     /* Unknown messageExtendedMedia CRC → -1 */
    1466            2 :     tl_writer_init(&w);
    1467            2 :     tl_write_uint32(&w, 0xa8852491u);
    1468            2 :     tl_write_int64 (&w, 5LL);
    1469            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1470            2 :     tl_write_uint32(&w, 0xFF00FFABu);
    1471            2 :     tl_write_uint32(&w, 0);
    1472            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
    1473            2 :     ASSERT(tl_skip_message_media_ex(&r4, NULL) == -1,
    1474              :            "unknown extMedia CRC rejected");
    1475            2 :     tl_writer_free(&w);
    1476              : }
    1477              : 
    1478              : /* ---- messageMediaStory → skip_story_item, skip_story_views,
    1479              :  *     skip_privacy_rule, skip_media_area, skip_geo_point_address,
    1480              :  *     skip_media_area_coordinates, skip_story_fwd_header ---- */
    1481            2 : static void test_media_story(void) {
    1482              :     /* Story with storyItemDeleted (simplest). */
    1483            2 :     TlWriter w; tl_writer_init(&w);
    1484            2 :     tl_write_uint32(&w, 0x68cb6283u);          /* messageMediaStory */
    1485            2 :     tl_write_uint32(&w, (1u << 0));            /* flags.0 → story present */
    1486            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 1LL);
    1487            2 :     tl_write_int32 (&w, 101);                  /* id */
    1488              :     /* storyItemDeleted#51e6ee4f id:int */
    1489            2 :     tl_write_uint32(&w, 0x51e6ee4fu);
    1490            2 :     tl_write_int32 (&w, 101);
    1491            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1492            2 :     MediaInfo mi = {0};
    1493            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "story deleted walked");
    1494            2 :     ASSERT(mi.kind == MEDIA_STORY, "kind=STORY");
    1495            2 :     tl_writer_free(&w);
    1496              : 
    1497              :     /* storyItemSkipped */
    1498            2 :     tl_writer_init(&w);
    1499            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1500            2 :     tl_write_uint32(&w, (1u << 0));
    1501            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 2LL);
    1502            2 :     tl_write_int32 (&w, 200);
    1503            2 :     tl_write_uint32(&w, 0xffadc913u);          /* storyItemSkipped */
    1504            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1505            2 :     tl_write_int32 (&w, 200);                  /* id */
    1506            2 :     tl_write_int32 (&w, 1700001000);           /* date */
    1507            2 :     tl_write_int32 (&w, 1700087400);           /* expire_date */
    1508            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1509            2 :     ASSERT(tl_skip_message_media_ex(&r2, NULL) == 0, "story skipped walked");
    1510            2 :     ASSERT(r2.pos == r2.len, "reader consumed story skipped");
    1511            2 :     tl_writer_free(&w);
    1512              : 
    1513              :     /* Full storyItem with caption + entities + mediaAreaVenue + privacyRule
    1514              :      * + storyViews + storyFwdHeader. Drives all the 0% static helpers. */
    1515            2 :     tl_writer_init(&w);
    1516            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1517            2 :     tl_write_uint32(&w, (1u << 0));
    1518            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 3LL);
    1519            2 :     tl_write_int32 (&w, 300);
    1520              :     /* storyItem#79b26a24 */
    1521            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1522              :     /* flags: bit 0=caption, 1=entities, 2=privacy, 3=views, 14=media_areas,
    1523              :      *        17=fwd_from (storyFwdHeader) */
    1524            2 :     uint32_t sflags = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) | (1u << 14) | (1u << 17);
    1525            2 :     tl_write_uint32(&w, sflags);
    1526            2 :     tl_write_int32 (&w, 300);                  /* id */
    1527            2 :     tl_write_int32 (&w, 1700001000);           /* date */
    1528              :     /* fwd_from: storyFwdHeader#b826e150 flags=0b110 (from_name + story_id) */
    1529            2 :     tl_write_uint32(&w, 0xb826e150u);
    1530            2 :     tl_write_uint32(&w, (1u << 1) | (1u << 2));
    1531            2 :     tl_write_string(&w, "Original Poster");   /* from_name */
    1532            2 :     tl_write_int32 (&w, 77);                  /* story_id */
    1533            2 :     tl_write_int32 (&w, 1700087400);           /* expire_date */
    1534            2 :     tl_write_string(&w, "Hello from story"); /* caption */
    1535              :     /* entities: Vector<MessageEntity> (bold) */
    1536            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1537            2 :     tl_write_uint32(&w, 0xbd610bc9u);          /* messageEntityBold */
    1538            2 :     tl_write_int32 (&w, 0); tl_write_int32(&w, 5);
    1539              :     /* media: messageMediaEmpty */
    1540            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1541              :     /* media_areas (flags.14): Vector<MediaArea> with two variants */
    1542            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1543              :     /* mediaAreaUrl: coordinates + url */
    1544            2 :     tl_write_uint32(&w, 0x37381085u);
    1545              :     /* mediaAreaCoordinates#03d1ea4e flags=0 x y w h rotation */
    1546            2 :     tl_write_uint32(&w, 0x03d1ea4eu);
    1547            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1548            2 :     double dvals[5] = {0.1, 0.2, 0.3, 0.4, 0.0};
    1549           12 :     for (int i = 0; i < 5; i++) {
    1550           10 :         uint64_t bits; __builtin_memcpy(&bits, &dvals[i], 8);
    1551           10 :         tl_write_uint32(&w, (uint32_t)bits);
    1552           10 :         tl_write_uint32(&w, (uint32_t)(bits >> 32));
    1553              :     }
    1554            2 :     tl_write_string(&w, "https://example.com");
    1555              :     /* mediaAreaGeoPoint with flags.0 → geoPointAddress */
    1556            2 :     tl_write_uint32(&w, 0xdf8b3b22u);
    1557            2 :     tl_write_uint32(&w, (1u << 0));            /* flags.0 → address present */
    1558              :     /* coordinates */
    1559            2 :     tl_write_uint32(&w, 0x03d1ea4eu);
    1560            2 :     tl_write_uint32(&w, 0);
    1561           12 :     for (int i = 0; i < 5; i++) {
    1562           10 :         uint64_t bits; __builtin_memcpy(&bits, &dvals[i], 8);
    1563           10 :         tl_write_uint32(&w, (uint32_t)bits);
    1564           10 :         tl_write_uint32(&w, (uint32_t)(bits >> 32));
    1565              :     }
    1566              :     /* geoPoint#b2a2f663 flags=0 long lat access_hash */
    1567            2 :     tl_write_uint32(&w, 0xb2a2f663u);
    1568            2 :     tl_write_uint32(&w, 0);
    1569              :     {
    1570            2 :         double lng = 13.4, lat = 52.5, acc = 0.0; uint64_t b;
    1571            2 :         __builtin_memcpy(&b, &lng, 8);
    1572            2 :         tl_write_uint32(&w, (uint32_t)b); tl_write_uint32(&w, (uint32_t)(b>>32));
    1573            2 :         __builtin_memcpy(&b, &lat, 8);
    1574            2 :         tl_write_uint32(&w, (uint32_t)b); tl_write_uint32(&w, (uint32_t)(b>>32));
    1575              :         (void)acc;
    1576            2 :         tl_write_int64(&w, 0LL);
    1577              :     }
    1578              :     /* geoPointAddress#de4c5d93 flags=0b111 country state city street */
    1579            2 :     tl_write_uint32(&w, 0xde4c5d93u);
    1580            2 :     tl_write_uint32(&w, 0x7u);
    1581            2 :     tl_write_string(&w, "DE");
    1582            2 :     tl_write_string(&w, "Berlin");
    1583            2 :     tl_write_string(&w, "Berlin");
    1584            2 :     tl_write_string(&w, "Unter den Linden");
    1585              :     /* privacy: Vector<PrivacyRule> with two variants (no-payload + users) */
    1586            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1587            2 :     tl_write_uint32(&w, 0xfffe1bacu);          /* privacyValueAllowContacts */
    1588            2 :     tl_write_uint32(&w, 0xb8905fb2u);          /* privacyValueAllowUsers */
    1589            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1590            2 :     tl_write_int64 (&w, 11LL); tl_write_int64(&w, 22LL);
    1591              :     /* views: storyViews#8d595cd6 flags=0b11101 */
    1592            2 :     tl_write_uint32(&w, 0x8d595cd6u);
    1593            2 :     uint32_t vflags = (1u << 0) | (1u << 2) | (1u << 3) | (1u << 4);
    1594            2 :     tl_write_uint32(&w, vflags);
    1595            2 :     tl_write_int32 (&w, 100);                  /* views_count */
    1596            2 :     tl_write_int32 (&w, 5);                    /* forwards_count */
    1597              :     /* reactions: Vector<ReactionCount> */
    1598            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1599            2 :     tl_write_uint32(&w, 0xa3d1cb80u);
    1600            2 :     tl_write_uint32(&w, 0);
    1601            2 :     tl_write_uint32(&w, 0x79f5d419u);          /* reactionEmpty */
    1602            2 :     tl_write_int32 (&w, 3);
    1603            2 :     tl_write_int32 (&w, 10);                   /* reactions_count */
    1604              :     /* recent_viewers: Vector<long> */
    1605            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1606            2 :     tl_write_int64 (&w, 99LL);
    1607            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1608            2 :     ASSERT(tl_skip_message_media_ex(&r3, NULL) == 0, "full story walked");
    1609            2 :     ASSERT(r3.pos == r3.len, "reader consumed full story");
    1610            2 :     tl_writer_free(&w);
    1611              : 
    1612              :     /* storyFwdHeader with from peer (flags.0) + story_id (flags.2). */
    1613            2 :     tl_writer_init(&w);
    1614            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1615            2 :     tl_write_uint32(&w, (1u << 0));
    1616            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 5LL);
    1617            2 :     tl_write_int32 (&w, 400);
    1618            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1619            2 :     uint32_t sfwdflags = (1u << 17);
    1620            2 :     tl_write_uint32(&w, sfwdflags);
    1621            2 :     tl_write_int32 (&w, 400);
    1622            2 :     tl_write_int32 (&w, 1700001000);
    1623            2 :     tl_write_uint32(&w, 0xb826e150u);
    1624            2 :     tl_write_uint32(&w, (1u << 0) | (1u << 2)); /* from_peer + story_id */
    1625            2 :     tl_write_uint32(&w, TL_peerChannel); tl_write_int64(&w, 10LL);
    1626            2 :     tl_write_int32 (&w, 55);
    1627            2 :     tl_write_int32 (&w, 1700087400);
    1628            2 :     tl_write_uint32(&w, 0x3ded6320u);          /* media: empty */
    1629            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
    1630            2 :     ASSERT(tl_skip_message_media_ex(&r4, NULL) == 0,
    1631              :            "story fwdHeader peer+storyId walked");
    1632            2 :     ASSERT(r4.pos == r4.len, "consumed story fwdHeader");
    1633            2 :     tl_writer_free(&w);
    1634              : 
    1635              :     /* Unknown storyItem CRC → -1 */
    1636            2 :     tl_writer_init(&w);
    1637            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1638            2 :     tl_write_uint32(&w, (1u << 0));
    1639            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 6LL);
    1640            2 :     tl_write_int32 (&w, 500);
    1641            2 :     tl_write_uint32(&w, 0xFF00AAAAu);          /* unknown storyItem */
    1642            2 :     tl_write_uint32(&w, 0);
    1643            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
    1644            2 :     ASSERT(tl_skip_message_media_ex(&r5, NULL) == -1,
    1645              :            "unknown storyItem rejected");
    1646            2 :     tl_writer_free(&w);
    1647              : 
    1648              :     /* mediaAreaSuggestedReaction */
    1649            2 :     tl_writer_init(&w);
    1650            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1651            2 :     tl_write_uint32(&w, (1u << 0));
    1652            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 7LL);
    1653            2 :     tl_write_int32 (&w, 600);
    1654            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1655            2 :     uint32_t sarf = (1u << 14);
    1656            2 :     tl_write_uint32(&w, sarf);
    1657            2 :     tl_write_int32 (&w, 600); tl_write_int32(&w, 1700001000);
    1658            2 :     tl_write_int32 (&w, 1700087400);
    1659            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1660              :     /* media_areas Vector with mediaAreaSuggestedReaction */
    1661            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1662            2 :     tl_write_uint32(&w, 0x14455871u);          /* mediaAreaSuggestedReaction */
    1663            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1664              :     /* coordinates */
    1665            2 :     tl_write_uint32(&w, 0x03d1ea4eu);
    1666            2 :     tl_write_uint32(&w, 0);
    1667           12 :     for (int i = 0; i < 5; i++) {
    1668           10 :         uint64_t bits; __builtin_memcpy(&bits, &dvals[i], 8);
    1669           10 :         tl_write_uint32(&w, (uint32_t)bits); tl_write_uint32(&w, (uint32_t)(bits>>32));
    1670              :     }
    1671            2 :     tl_write_uint32(&w, 0x79f5d419u);          /* reactionEmpty */
    1672            2 :     TlReader r6 = tl_reader_init(w.data, w.len);
    1673            2 :     ASSERT(tl_skip_message_media_ex(&r6, NULL) == 0,
    1674              :            "mediaAreaSuggestedReaction walked");
    1675            2 :     ASSERT(r6.pos == r6.len, "consumed suggestedReaction");
    1676            2 :     tl_writer_free(&w);
    1677              : 
    1678              :     /* mediaAreaChannelPost, mediaAreaWeather, mediaAreaStarGift, mediaAreaVenue */
    1679            2 :     tl_writer_init(&w);
    1680            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1681            2 :     tl_write_uint32(&w, (1u << 0));
    1682            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 8LL);
    1683            2 :     tl_write_int32 (&w, 700);
    1684            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1685            2 :     uint32_t s4f = (1u << 14);
    1686            2 :     tl_write_uint32(&w, s4f);
    1687            2 :     tl_write_int32 (&w, 700); tl_write_int32(&w, 1700001000);
    1688            2 :     tl_write_int32 (&w, 1700087400);
    1689            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1690              :     /* media_areas: Vector with 4 areas */
    1691            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 4);
    1692              :     /* helper macro: write bare coordinates (flags=0) */
    1693              : #define WRITE_COORDS(ww) do { \
    1694              :     tl_write_uint32(ww, 0x03d1ea4eu); tl_write_uint32(ww, 0); \
    1695              :     double _d[5] = {0.1,0.2,0.3,0.4,0.0}; \
    1696              :     for(int _i=0;_i<5;_i++){uint64_t _b; __builtin_memcpy(&_b,&_d[_i],8); \
    1697              :         tl_write_uint32(ww,(uint32_t)_b); tl_write_uint32(ww,(uint32_t)(_b>>32));} \
    1698              : } while(0)
    1699              :     /* mediaAreaChannelPost */
    1700            2 :     tl_write_uint32(&w, 0x770416afu);
    1701           12 :     WRITE_COORDS(&w);
    1702            2 :     tl_write_int64(&w, 12345LL);               /* channel_id */
    1703            2 :     tl_write_int32(&w, 777);                   /* msg_id */
    1704              :     /* mediaAreaWeather */
    1705            2 :     tl_write_uint32(&w, 0x49a6549cu);
    1706           12 :     WRITE_COORDS(&w);
    1707            2 :     tl_write_string(&w, "\xe2\x98\x80\xef\xb8\x8f"); /* emoji */
    1708            2 :     double temp = 22.5; uint64_t tbits; __builtin_memcpy(&tbits, &temp, 8);
    1709            2 :     tl_write_uint32(&w, (uint32_t)tbits); tl_write_uint32(&w, (uint32_t)(tbits>>32));
    1710            2 :     tl_write_int32(&w, 0xFFFFFFu);             /* color */
    1711              :     /* mediaAreaStarGift */
    1712            2 :     tl_write_uint32(&w, 0x5787686du);
    1713           12 :     WRITE_COORDS(&w);
    1714            2 :     tl_write_string(&w, "gift-slug-42");
    1715              :     /* mediaAreaVenue */
    1716            2 :     tl_write_uint32(&w, 0xbe82db9cu);
    1717           12 :     WRITE_COORDS(&w);
    1718            2 :     tl_write_uint32(&w, 0x1117dd5fu);          /* geoPointEmpty */
    1719            2 :     tl_write_string(&w, "Cafe");
    1720            2 :     tl_write_string(&w, "Main St 1");
    1721            2 :     tl_write_string(&w, "foursquare");
    1722            2 :     tl_write_string(&w, "v42");
    1723            2 :     tl_write_string(&w, "cafe");
    1724            2 :     TlReader r7 = tl_reader_init(w.data, w.len);
    1725            2 :     ASSERT(tl_skip_message_media_ex(&r7, NULL) == 0, "4 media area variants");
    1726            2 :     ASSERT(r7.pos == r7.len, "consumed 4 media areas");
    1727            2 :     tl_writer_free(&w);
    1728              : #undef WRITE_COORDS
    1729              : 
    1730              :     /* Unknown mediaArea CRC → -1 */
    1731            2 :     tl_writer_init(&w);
    1732            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1733            2 :     tl_write_uint32(&w, (1u << 0));
    1734            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 9LL);
    1735            2 :     tl_write_int32 (&w, 800);
    1736            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1737            2 :     tl_write_uint32(&w, (1u << 14));
    1738            2 :     tl_write_int32 (&w, 800); tl_write_int32(&w, 1700001000);
    1739            2 :     tl_write_int32 (&w, 1700087400);
    1740            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1741            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1742            2 :     tl_write_uint32(&w, 0xFF00DDDDu);          /* unknown mediaArea */
    1743            2 :     tl_write_uint32(&w, 0);
    1744            2 :     TlReader r8 = tl_reader_init(w.data, w.len);
    1745            2 :     ASSERT(tl_skip_message_media_ex(&r8, NULL) == -1, "unknown mediaArea rejected");
    1746            2 :     tl_writer_free(&w);
    1747              : 
    1748              :     /* Unknown privacyRule CRC → -1 */
    1749            2 :     tl_writer_init(&w);
    1750            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1751            2 :     tl_write_uint32(&w, (1u << 0));
    1752            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 10LL);
    1753            2 :     tl_write_int32 (&w, 900);
    1754            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1755            2 :     tl_write_uint32(&w, (1u << 2));            /* only privacy flag */
    1756            2 :     tl_write_int32 (&w, 900); tl_write_int32(&w, 1700001000);
    1757            2 :     tl_write_int32 (&w, 1700087400);
    1758            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1759            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1760            2 :     tl_write_uint32(&w, 0xFF00CCCCu);          /* unknown privacyRule */
    1761            2 :     TlReader r9 = tl_reader_init(w.data, w.len);
    1762            2 :     ASSERT(tl_skip_message_media_ex(&r9, NULL) == -1, "unknown privacyRule rejected");
    1763            2 :     tl_writer_free(&w);
    1764              : 
    1765              :     /* storyViews unknown CRC → -1 */
    1766            2 :     tl_writer_init(&w);
    1767            2 :     tl_write_uint32(&w, 0x68cb6283u);
    1768            2 :     tl_write_uint32(&w, (1u << 0));
    1769            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 11LL);
    1770            2 :     tl_write_int32 (&w, 1000);
    1771            2 :     tl_write_uint32(&w, 0x79b26a24u);
    1772            2 :     tl_write_uint32(&w, (1u << 3));            /* only views flag */
    1773            2 :     tl_write_int32 (&w, 1000); tl_write_int32(&w, 1700001000);
    1774            2 :     tl_write_int32 (&w, 1700087400);
    1775            2 :     tl_write_uint32(&w, 0x3ded6320u);
    1776            2 :     tl_write_uint32(&w, 0xFF00BBBBu);          /* unknown storyViews */
    1777            2 :     tl_write_uint32(&w, 0);
    1778            2 :     TlReader r10 = tl_reader_init(w.data, w.len);
    1779            2 :     ASSERT(tl_skip_message_media_ex(&r10, NULL) == -1,
    1780              :            "unknown storyViews rejected");
    1781            2 :     tl_writer_free(&w);
    1782              : }
    1783              : 
    1784              : /* ---- messageMediaPoll → skip_poll_answer_voters ---- */
    1785            2 : static void test_media_poll_with_results(void) {
    1786              :     /* Build a poll + results that exercises skip_poll_answer_voters. */
    1787            2 :     TlWriter w; tl_writer_init(&w);
    1788            2 :     tl_write_uint32(&w, 0x4bd6e798u);          /* messageMediaPoll */
    1789              :     /* poll#58747131 flags=0 id question answers */
    1790            2 :     tl_write_uint32(&w, 0x58747131u);
    1791            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1792            2 :     tl_write_int64 (&w, 77LL);                 /* id */
    1793              :     /* question: textWithEntities#751f3146 */
    1794            2 :     tl_write_uint32(&w, 0x751f3146u);
    1795            2 :     tl_write_string(&w, "Best fruit?");
    1796            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1797              :     /* answers: Vector<PollAnswer> — two answers */
    1798            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1799              :     /* PollAnswer#6ca9c2e9 text:TextWithEntities option:bytes */
    1800            2 :     tl_write_uint32(&w, 0x6ca9c2e9u);
    1801            2 :     tl_write_uint32(&w, 0x751f3146u); tl_write_string(&w, "Apple");
    1802            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1803            2 :     tl_write_bytes (&w, (const unsigned char *)"\x01", 1);
    1804            2 :     tl_write_uint32(&w, 0x6ca9c2e9u);
    1805            2 :     tl_write_uint32(&w, 0x751f3146u); tl_write_string(&w, "Banana");
    1806            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1807            2 :     tl_write_bytes (&w, (const unsigned char *)"\x02", 1);
    1808              :     /* PollResults#7adc669d flags=0b1110 (results + total_voters + recent) */
    1809            2 :     tl_write_uint32(&w, 0x7adc669du);
    1810            2 :     uint32_t rflags = (1u << 1) | (1u << 2) | (1u << 3);
    1811            2 :     tl_write_uint32(&w, rflags);
    1812              :     /* results: Vector<PollAnswerVoters> */
    1813            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    1814              :     /* PollAnswerVoters#3b6ddad2 flags option:bytes voters */
    1815            2 :     tl_write_uint32(&w, 0x3b6ddad2u);
    1816            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1817            2 :     tl_write_bytes (&w, (const unsigned char *)"\x01", 1);
    1818            2 :     tl_write_int32 (&w, 30);
    1819            2 :     tl_write_uint32(&w, 0x3b6ddad2u);
    1820            2 :     tl_write_uint32(&w, 0);
    1821            2 :     tl_write_bytes (&w, (const unsigned char *)"\x02", 1);
    1822            2 :     tl_write_int32 (&w, 20);
    1823            2 :     tl_write_int32 (&w, 50);                   /* total_voters */
    1824              :     /* recent_voters: Vector<Peer> */
    1825            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1826            2 :     tl_write_uint32(&w, TL_peerUser); tl_write_int64(&w, 42LL);
    1827            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1828            2 :     MediaInfo mi = {0};
    1829            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "poll+results walked");
    1830            2 :     ASSERT(mi.kind == MEDIA_POLL, "kind=POLL");
    1831            2 :     ASSERT(r.pos == r.len, "reader consumed poll");
    1832            2 :     tl_writer_free(&w);
    1833              : 
    1834              :     /* PollResults with solution (flags.4). */
    1835            2 :     tl_writer_init(&w);
    1836            2 :     tl_write_uint32(&w, 0x4bd6e798u);
    1837            2 :     tl_write_uint32(&w, 0x58747131u);
    1838            2 :     tl_write_uint32(&w, 0);
    1839            2 :     tl_write_int64 (&w, 78LL);
    1840            2 :     tl_write_uint32(&w, 0x751f3146u); tl_write_string(&w, "Q?");
    1841            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1842            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1843            2 :     tl_write_uint32(&w, 0x7adc669du);
    1844            2 :     tl_write_uint32(&w, (1u << 4));            /* solution flag */
    1845            2 :     tl_write_string(&w, "Answer is 42");
    1846            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1847            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1848            2 :     ASSERT(tl_skip_message_media_ex(&r2, NULL) == 0, "poll+solution walked");
    1849            2 :     tl_writer_free(&w);
    1850              : 
    1851              :     /* Bad PollAnswerVoters CRC → -1 */
    1852            2 :     tl_writer_init(&w);
    1853            2 :     tl_write_uint32(&w, 0x4bd6e798u);
    1854            2 :     tl_write_uint32(&w, 0x58747131u);
    1855            2 :     tl_write_uint32(&w, 0);
    1856            2 :     tl_write_int64 (&w, 79LL);
    1857            2 :     tl_write_uint32(&w, 0x751f3146u); tl_write_string(&w, "Q?");
    1858            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1859            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1860            2 :     tl_write_uint32(&w, 0x7adc669du);
    1861            2 :     tl_write_uint32(&w, (1u << 1));
    1862            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    1863            2 :     tl_write_uint32(&w, 0xFF00EEEEu);          /* bad PollAnswerVoters */
    1864            2 :     tl_write_uint32(&w, 0);
    1865            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1866            2 :     ASSERT(tl_skip_message_media_ex(&r3, NULL) == -1,
    1867              :            "bad PollAnswerVoters rejected");
    1868            2 :     tl_writer_free(&w);
    1869              : }
    1870              : 
    1871              : /* ---- messageMediaWebPage → skip_webpage + deep nested page/block/rich ---- */
    1872            2 : static void test_media_webpage_variants(void) {
    1873              :     /* webPageEmpty */
    1874            2 :     TlWriter w; tl_writer_init(&w);
    1875            2 :     tl_write_uint32(&w, 0xddf8c26eu);          /* messageMediaWebPage */
    1876            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1877            2 :     tl_write_uint32(&w, 0xeb1477e8u);          /* webPageEmpty */
    1878            2 :     tl_write_uint32(&w, 0);                    /* flags */
    1879            2 :     tl_write_int64 (&w, 1LL);                  /* id */
    1880            2 :     TlReader r = tl_reader_init(w.data, w.len);
    1881            2 :     MediaInfo mi = {0};
    1882            2 :     ASSERT(tl_skip_message_media_ex(&r, &mi) == 0, "webPageEmpty walked");
    1883            2 :     ASSERT(mi.kind == MEDIA_WEBPAGE, "kind=WEBPAGE");
    1884            2 :     tl_writer_free(&w);
    1885              : 
    1886              :     /* webPagePending */
    1887            2 :     tl_writer_init(&w);
    1888            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    1889            2 :     tl_write_uint32(&w, 0);
    1890            2 :     tl_write_uint32(&w, 0xb0d13e47u);          /* webPagePending */
    1891            2 :     tl_write_uint32(&w, 0);
    1892            2 :     tl_write_int64 (&w, 2LL);
    1893            2 :     tl_write_int32 (&w, 1700000000);           /* date */
    1894            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    1895            2 :     ASSERT(tl_skip_message_media_ex(&r2, NULL) == 0, "webPagePending walked");
    1896            2 :     tl_writer_free(&w);
    1897              : 
    1898              :     /* webPageNotModified (no flags) */
    1899            2 :     tl_writer_init(&w);
    1900            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    1901            2 :     tl_write_uint32(&w, 0);
    1902            2 :     tl_write_uint32(&w, 0x7311ca11u);          /* webPageNotModified */
    1903            2 :     tl_write_uint32(&w, (1u << 0));            /* flags.0 → cached_page_views */
    1904            2 :     tl_write_int32 (&w, 1234);                 /* cached_page_views */
    1905            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    1906            2 :     ASSERT(tl_skip_message_media_ex(&r3, NULL) == 0, "webPageNotModified walked");
    1907            2 :     tl_writer_free(&w);
    1908              : 
    1909              :     /* Full webPage — type + site + title + description + embed + author,
    1910              :      * flags 0,1,2,3,5,6,7,8 — no photo/document/page/attributes. */
    1911            2 :     tl_writer_init(&w);
    1912            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    1913            2 :     tl_write_uint32(&w, 0);
    1914            2 :     tl_write_uint32(&w, 0xe89c45b2u);          /* webPage */
    1915            2 :     uint32_t wpf = (1u<<0)|(1u<<1)|(1u<<2)|(1u<<3)|(1u<<5)|(1u<<6)|(1u<<7)|(1u<<8);
    1916            2 :     tl_write_uint32(&w, wpf);
    1917            2 :     tl_write_int64 (&w, 99LL);
    1918            2 :     tl_write_string(&w, "https://example.com");
    1919            2 :     tl_write_string(&w, "example.com");
    1920            2 :     tl_write_int32 (&w, 0);                    /* hash */
    1921            2 :     tl_write_string(&w, "article");            /* type */
    1922            2 :     tl_write_string(&w, "Example Site");       /* site_name */
    1923            2 :     tl_write_string(&w, "My Title");           /* title */
    1924            2 :     tl_write_string(&w, "Description here");  /* description */
    1925            2 :     tl_write_string(&w, "https://embed.example.com");
    1926            2 :     tl_write_string(&w, "text/html");          /* embed_type */
    1927            2 :     tl_write_int32 (&w, 640); tl_write_int32(&w, 360);
    1928            2 :     tl_write_int32 (&w, 120);                  /* duration */
    1929            2 :     tl_write_string(&w, "Author Name");
    1930            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
    1931            2 :     ASSERT(tl_skip_message_media_ex(&r4, NULL) == 0, "full webPage walked");
    1932            2 :     ASSERT(r4.pos == r4.len, "consumed full webPage");
    1933            2 :     tl_writer_free(&w);
    1934              : 
    1935              :     /* webPage with cached_page (flags.10) — exercises skip_page + skip_page_block. */
    1936            2 :     tl_writer_init(&w);
    1937            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    1938            2 :     tl_write_uint32(&w, 0);
    1939            2 :     tl_write_uint32(&w, 0xe89c45b2u);
    1940            2 :     uint32_t pgf = (1u << 10);                 /* cached_page */
    1941            2 :     tl_write_uint32(&w, pgf);
    1942            2 :     tl_write_int64 (&w, 100LL);
    1943            2 :     tl_write_string(&w, "https://telegraph.ph/article");
    1944            2 :     tl_write_string(&w, "telegraph.ph");
    1945            2 :     tl_write_int32 (&w, 0);
    1946              :     /* page#98657f0d flags=0 url blocks photos docs */
    1947            2 :     tl_write_uint32(&w, 0x98657f0du);
    1948            2 :     tl_write_uint32(&w, 0);
    1949            2 :     tl_write_string(&w, "https://telegraph.ph/article");
    1950              :     /* blocks: Vector<PageBlock> — cover several block variants */
    1951            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 8);
    1952              :     /* pageBlockUnsupported */
    1953            2 :     tl_write_uint32(&w, 0x13567e8au);
    1954              :     /* pageBlockDivider */
    1955            2 :     tl_write_uint32(&w, 0xdb20b188u);
    1956              :     /* pageBlockTitle: rich text textPlain */
    1957            2 :     tl_write_uint32(&w, 0x70abc3fdu);
    1958            2 :     tl_write_uint32(&w, 0x744694e0u); tl_write_string(&w, "My Title");
    1959              :     /* pageBlockParagraph: textBold wrapping textPlain */
    1960            2 :     tl_write_uint32(&w, 0x467a0766u);
    1961            2 :     tl_write_uint32(&w, 0x6724abc4u);          /* textBold */
    1962            2 :     tl_write_uint32(&w, 0x744694e0u); tl_write_string(&w, "bold");
    1963              :     /* pageBlockAnchor: string */
    1964            2 :     tl_write_uint32(&w, 0xce0d37b0u);
    1965            2 :     tl_write_string(&w, "anchor1");
    1966              :     /* pageBlockPreformatted: rich + language string */
    1967            2 :     tl_write_uint32(&w, 0xc070d93eu);
    1968            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* textEmpty */
    1969            2 :     tl_write_string(&w, "python");
    1970              :     /* pageBlockAuthorDate: rich + int */
    1971            2 :     tl_write_uint32(&w, 0xbaafe5e0u);
    1972            2 :     tl_write_uint32(&w, 0x744694e0u); tl_write_string(&w, "Author");
    1973            2 :     tl_write_int32 (&w, 1700001000);
    1974              :     /* pageBlockBlockquote: two rich texts */
    1975            2 :     tl_write_uint32(&w, 0x263d7c26u);
    1976            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* textEmpty */
    1977            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    1978              :     /* photos: Vector<Photo> — empty */
    1979            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1980              :     /* docs: Vector<Document> — empty */
    1981            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    1982            2 :     TlReader r5 = tl_reader_init(w.data, w.len);
    1983            2 :     ASSERT(tl_skip_message_media_ex(&r5, NULL) == 0, "webPage+page walked");
    1984            2 :     ASSERT(r5.pos == r5.len, "consumed page");
    1985            2 :     tl_writer_free(&w);
    1986              : 
    1987              :     /* Page with more block variants: Cover, List, OrderedList, Table,
    1988              :      * RelatedArticles, Collage, Details. */
    1989            2 :     tl_writer_init(&w);
    1990            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    1991            2 :     tl_write_uint32(&w, 0);
    1992            2 :     tl_write_uint32(&w, 0xe89c45b2u);
    1993            2 :     tl_write_uint32(&w, (1u << 10));
    1994            2 :     tl_write_int64 (&w, 101LL);
    1995            2 :     tl_write_string(&w, "https://t.me/a");
    1996            2 :     tl_write_string(&w, "t.me");
    1997            2 :     tl_write_int32 (&w, 0);
    1998            2 :     tl_write_uint32(&w, 0x98657f0du);
    1999            2 :     tl_write_uint32(&w, 0);
    2000            2 :     tl_write_string(&w, "https://t.me/a");
    2001            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 7);
    2002              :     /* pageBlockCover wrapping a pageBlockDivider */
    2003            2 :     tl_write_uint32(&w, 0x39f23300u);
    2004            2 :     tl_write_uint32(&w, 0xdb20b188u);
    2005              :     /* pageBlockList: one text item */
    2006            2 :     tl_write_uint32(&w, 0xe4e88011u);
    2007            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2008            2 :     tl_write_uint32(&w, 0xb92fb6cdu);          /* pageListItemText */
    2009            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* textEmpty */
    2010              :     /* pageBlockOrderedList: one ordered text item */
    2011            2 :     tl_write_uint32(&w, 0x9a8ae1e1u);
    2012            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2013            2 :     tl_write_uint32(&w, 0x5e068047u);          /* pageListOrderedItemText */
    2014            2 :     tl_write_string(&w, "1.");
    2015            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2016              :     /* pageBlockTable: flags=0 title rows */
    2017            2 :     tl_write_uint32(&w, 0xbf4dea82u);
    2018            2 :     tl_write_uint32(&w, 0);
    2019            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* title: textEmpty */
    2020            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2021              :     /* pageTableRow: Vector<pageTableCell> with one cell */
    2022            2 :     tl_write_uint32(&w, 0xe0c0c5e5u);
    2023            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2024              :     /* pageTableCell: flags=0b11000010 (text=bit7, colspan=bit1, rowspan=bit2) */
    2025            2 :     tl_write_uint32(&w, 0x34566b6au);
    2026            2 :     tl_write_uint32(&w, (1u<<7)|(1u<<1)|(1u<<2));
    2027            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* text: textEmpty */
    2028            2 :     tl_write_int32 (&w, 2);                    /* colspan */
    2029            2 :     tl_write_int32 (&w, 1);                    /* rowspan */
    2030              :     /* pageBlockRelatedArticles: title + articles */
    2031            2 :     tl_write_uint32(&w, 0x16115a96u);
    2032            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* title */
    2033            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2034              :     /* pageRelatedArticle#b390dc08 flags=0b11111 */
    2035            2 :     tl_write_uint32(&w, 0xb390dc08u);
    2036            2 :     tl_write_uint32(&w, 0x1fu);
    2037            2 :     tl_write_string(&w, "https://rel.example.com");
    2038            2 :     tl_write_int64 (&w, 5LL);
    2039            2 :     tl_write_string(&w, "Related Title");
    2040            2 :     tl_write_string(&w, "Related Desc");
    2041            2 :     tl_write_int64 (&w, 6LL);
    2042            2 :     tl_write_string(&w, "Rel Author");
    2043            2 :     tl_write_int32 (&w, 1700000000);
    2044              :     /* pageBlockDetails: flags=0 blocks title */
    2045            2 :     tl_write_uint32(&w, 0x76768bedu);
    2046            2 :     tl_write_uint32(&w, 0);
    2047            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2048            2 :     tl_write_uint32(&w, 0xdc3d824fu);          /* title */
    2049              :     /* pageBlockCollage: blocks caption */
    2050            2 :     tl_write_uint32(&w, 0x65a0fa4du);
    2051            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2052              :     /* caption: pageCaption#6f747657 text:RichText credit:RichText */
    2053            2 :     tl_write_uint32(&w, 0x6f747657u);
    2054            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2055            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2056            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2057            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2058            2 :     TlReader r6 = tl_reader_init(w.data, w.len);
    2059            2 :     ASSERT(tl_skip_message_media_ex(&r6, NULL) == 0, "page block variants walked");
    2060            2 :     ASSERT(r6.pos == r6.len, "consumed page block variants");
    2061            2 :     tl_writer_free(&w);
    2062              : 
    2063              :     /* Rich text variants: textUrl, textEmail, textPhone, textConcat,
    2064              :      * textImage, textAnchor. */
    2065            2 :     tl_writer_init(&w);
    2066            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    2067            2 :     tl_write_uint32(&w, 0);
    2068            2 :     tl_write_uint32(&w, 0xe89c45b2u);
    2069            2 :     tl_write_uint32(&w, (1u << 10));
    2070            2 :     tl_write_int64 (&w, 102LL);
    2071            2 :     tl_write_string(&w, "https://t.me/b"); tl_write_string(&w, "t.me");
    2072            2 :     tl_write_int32 (&w, 0);
    2073            2 :     tl_write_uint32(&w, 0x98657f0du);
    2074            2 :     tl_write_uint32(&w, 0);
    2075            2 :     tl_write_string(&w, "https://t.me/b");
    2076            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2077              :     /* pageBlockPullquote: two rich text fields — use complex rich texts */
    2078            2 :     tl_write_uint32(&w, 0x4f4456d5u);
    2079              :     /* text: textConcat wrapping [textUrl, textEmail, textPhone, textImage, textAnchor] */
    2080            2 :     tl_write_uint32(&w, 0x7e6260d7u);          /* textConcat */
    2081            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 5);
    2082              :     /* textUrl: rich + url + webpage_id */
    2083            2 :     tl_write_uint32(&w, 0x3c2884c1u);
    2084            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2085            2 :     tl_write_string(&w, "https://link.example.com");
    2086            2 :     tl_write_int64 (&w, 7LL);
    2087              :     /* textEmail: rich + email */
    2088            2 :     tl_write_uint32(&w, 0xde5a0dd6u);
    2089            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2090            2 :     tl_write_string(&w, "a@b.com");
    2091              :     /* textPhone: rich + phone */
    2092            2 :     tl_write_uint32(&w, 0x1ccb966au);
    2093            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2094            2 :     tl_write_string(&w, "+15550001234");
    2095              :     /* textImage: document_id w h */
    2096            2 :     tl_write_uint32(&w, 0x081ccf4fu);
    2097            2 :     tl_write_int64 (&w, 8LL);
    2098            2 :     tl_write_int32 (&w, 32); tl_write_int32(&w, 32);
    2099              :     /* textAnchor: rich + name */
    2100            2 :     tl_write_uint32(&w, 0x35553762u);
    2101            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2102            2 :     tl_write_string(&w, "sec1");
    2103              :     /* credit: textEmpty */
    2104            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2105            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2106            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2107            2 :     TlReader r7 = tl_reader_init(w.data, w.len);
    2108            2 :     ASSERT(tl_skip_message_media_ex(&r7, NULL) == 0, "rich text variants walked");
    2109            2 :     ASSERT(r7.pos == r7.len, "consumed rich text variants");
    2110            2 :     tl_writer_free(&w);
    2111              : 
    2112              :     /* webPage with attributes (flags.12) — webPageAttributeTheme (no docs) +
    2113              :      * webPageAttributeStickerSet (empty stickers). */
    2114            2 :     tl_writer_init(&w);
    2115            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    2116            2 :     tl_write_uint32(&w, 0);
    2117            2 :     tl_write_uint32(&w, 0xe89c45b2u);
    2118            2 :     tl_write_uint32(&w, (1u << 12));
    2119            2 :     tl_write_int64 (&w, 103LL);
    2120            2 :     tl_write_string(&w, "https://t.me/c"); tl_write_string(&w, "t.me");
    2121            2 :     tl_write_int32 (&w, 0);
    2122              :     /* attributes: Vector<WebPageAttribute> */
    2123            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    2124              :     /* webPageAttributeTheme flags=0 (no documents, no settings) */
    2125            2 :     tl_write_uint32(&w, 0x54b56617u);
    2126            2 :     tl_write_uint32(&w, 0);
    2127              :     /* webPageAttributeStickerSet flags=0 stickers=[] */
    2128            2 :     tl_write_uint32(&w, 0x50cc03d3u);
    2129            2 :     tl_write_uint32(&w, 0);
    2130            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2131            2 :     TlReader r8 = tl_reader_init(w.data, w.len);
    2132            2 :     ASSERT(tl_skip_message_media_ex(&r8, NULL) == 0, "webPage attributes walked");
    2133            2 :     ASSERT(r8.pos == r8.len, "consumed webPage attributes");
    2134            2 :     tl_writer_free(&w);
    2135              : 
    2136              :     /* Unknown webPage CRC → -1 */
    2137            2 :     tl_writer_init(&w);
    2138            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    2139            2 :     tl_write_uint32(&w, 0);
    2140            2 :     tl_write_uint32(&w, 0xFF001234u);
    2141            2 :     tl_write_uint32(&w, 0);
    2142            2 :     TlReader r9 = tl_reader_init(w.data, w.len);
    2143            2 :     ASSERT(tl_skip_message_media_ex(&r9, NULL) == -1, "unknown webPage rejected");
    2144            2 :     tl_writer_free(&w);
    2145              : }
    2146              : 
    2147              : /* ---- Chat with admin rights + banned rights ---- */
    2148            2 : static void test_chat_admin_banned_rights(void) {
    2149              :     /* chat#41cbf256 with flags.14 (admin_rights) and flags.18 (banned_rights). */
    2150            2 :     TlWriter w; tl_writer_init(&w);
    2151            2 :     tl_write_uint32(&w, TL_chat);
    2152            2 :     uint32_t cflags = (1u << 14) | (1u << 18);
    2153            2 :     tl_write_uint32(&w, cflags);
    2154            2 :     tl_write_int64 (&w, 1001LL);
    2155            2 :     tl_write_string(&w, "Test Group");
    2156              :     /* chatPhotoEmpty */
    2157            2 :     tl_write_uint32(&w, 0x37c1011cu);
    2158            2 :     tl_write_int32 (&w, 5);                    /* participants_count */
    2159            2 :     tl_write_int32 (&w, 1700000000);           /* date */
    2160            2 :     tl_write_int32 (&w, 1);                    /* version */
    2161              :     /* chatAdminRights#5fb224d5 flags:# */
    2162            2 :     tl_write_uint32(&w, 0x5fb224d5u);
    2163            2 :     tl_write_uint32(&w, 0x1ff);               /* all known bits */
    2164              :     /* chatBannedRights#9f120418 flags:# until_date:int */
    2165            2 :     tl_write_uint32(&w, 0x9f120418u);
    2166            2 :     tl_write_uint32(&w, 0);
    2167            2 :     tl_write_int32 (&w, 0);
    2168            2 :     TlReader r = tl_reader_init(w.data, w.len);
    2169            2 :     ChatSummary cs = {0};
    2170            2 :     ASSERT(tl_extract_chat(&r, &cs) == 0, "chat with admin+banned walked");
    2171            2 :     ASSERT(cs.id == 1001LL, "chat id captured");
    2172            2 :     ASSERT(r.pos == r.len, "reader consumed chat");
    2173            2 :     tl_writer_free(&w);
    2174              : 
    2175              :     /* channel with flags.14 (admin_rights) + flags.15 (banned_rights) +
    2176              :      * flags.18 (default_banned_rights). Minimal: no access_hash, no username. */
    2177            2 :     tl_writer_init(&w);
    2178            2 :     tl_write_uint32(&w, TL_channel);
    2179            2 :     uint32_t chflags = (1u << 14) | (1u << 15) | (1u << 18);
    2180            2 :     tl_write_uint32(&w, chflags);
    2181            2 :     tl_write_uint32(&w, 0);                    /* flags2 */
    2182            2 :     tl_write_int64 (&w, 2001LL);
    2183            2 :     tl_write_string(&w, "My Channel");
    2184            2 :     tl_write_uint32(&w, 0x37c1011cu);          /* chatPhotoEmpty */
    2185            2 :     tl_write_int32 (&w, 1700000000);           /* date */
    2186            2 :     tl_write_uint32(&w, 0x5fb224d5u);          /* chatAdminRights */
    2187            2 :     tl_write_uint32(&w, 0);
    2188            2 :     tl_write_uint32(&w, 0x9f120418u);          /* chatBannedRights (flags.15) */
    2189            2 :     tl_write_uint32(&w, 0); tl_write_int32(&w, 0);
    2190            2 :     tl_write_uint32(&w, 0x9f120418u);          /* default_banned_rights (flags.18) */
    2191            2 :     tl_write_uint32(&w, 0); tl_write_int32(&w, 0);
    2192            2 :     TlReader r2 = tl_reader_init(w.data, w.len);
    2193            2 :     ChatSummary cs2 = {0};
    2194            2 :     ASSERT(tl_extract_chat(&r2, &cs2) == 0, "channel with rights walked");
    2195            2 :     ASSERT(cs2.id == 2001LL, "channel id captured");
    2196            2 :     tl_writer_free(&w);
    2197              : 
    2198              :     /* chatAdminRights unknown CRC → -1 (reach via chat) */
    2199            2 :     tl_writer_init(&w);
    2200            2 :     tl_write_uint32(&w, TL_chat);
    2201            2 :     tl_write_uint32(&w, (1u << 14));
    2202            2 :     tl_write_int64 (&w, 1002LL);
    2203            2 :     tl_write_string(&w, "G");
    2204            2 :     tl_write_uint32(&w, 0x37c1011cu);
    2205            2 :     tl_write_int32 (&w, 0); tl_write_int32(&w, 0); tl_write_int32(&w, 0);
    2206            2 :     tl_write_uint32(&w, 0xFF00DDDDu);          /* unknown admin rights */
    2207            2 :     tl_write_uint32(&w, 0);
    2208            2 :     TlReader r3 = tl_reader_init(w.data, w.len);
    2209            2 :     ASSERT(tl_extract_chat(&r3, NULL) == -1, "unknown adminRights rejected");
    2210            2 :     tl_writer_free(&w);
    2211              : 
    2212              :     /* chatBannedRights unknown CRC → -1 */
    2213            2 :     tl_writer_init(&w);
    2214            2 :     tl_write_uint32(&w, TL_chat);
    2215            2 :     tl_write_uint32(&w, (1u << 18));
    2216            2 :     tl_write_int64 (&w, 1003LL);
    2217            2 :     tl_write_string(&w, "H");
    2218            2 :     tl_write_uint32(&w, 0x37c1011cu);
    2219            2 :     tl_write_int32 (&w, 0); tl_write_int32(&w, 0); tl_write_int32(&w, 0);
    2220            2 :     tl_write_uint32(&w, 0xFF00CCCCu);          /* unknown banned rights */
    2221            2 :     tl_write_uint32(&w, 0); tl_write_int32(&w, 0);
    2222            2 :     TlReader r4 = tl_reader_init(w.data, w.len);
    2223            2 :     ASSERT(tl_extract_chat(&r4, NULL) == -1, "unknown bannedRights rejected");
    2224            2 :     tl_writer_free(&w);
    2225              : }
    2226              : 
    2227              : /* ---- Remaining list/ordered list blocks variant (blocks variant) ---- */
    2228            2 : static void test_page_block_list_blocks_variant(void) {
    2229              :     /* Exercises pageListItemBlocks and pageListOrderedItemBlocks branches. */
    2230            2 :     TlWriter w; tl_writer_init(&w);
    2231            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    2232            2 :     tl_write_uint32(&w, 0);
    2233            2 :     tl_write_uint32(&w, 0xe89c45b2u);
    2234            2 :     tl_write_uint32(&w, (1u << 10));
    2235            2 :     tl_write_int64 (&w, 104LL);
    2236            2 :     tl_write_string(&w, "https://t.me/d"); tl_write_string(&w, "t.me");
    2237            2 :     tl_write_int32 (&w, 0);
    2238            2 :     tl_write_uint32(&w, 0x98657f0du);
    2239            2 :     tl_write_uint32(&w, 0);
    2240            2 :     tl_write_string(&w, "https://t.me/d");
    2241            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 2);
    2242              :     /* pageBlockList with a pageListItemBlocks item */
    2243            2 :     tl_write_uint32(&w, 0xe4e88011u);
    2244            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2245            2 :     tl_write_uint32(&w, 0x25e073fcu);          /* pageListItemBlocks */
    2246            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2247            2 :     tl_write_uint32(&w, 0xdb20b188u);          /* pageBlockDivider (leaf) */
    2248              :     /* pageBlockOrderedList with a pageListOrderedItemBlocks item */
    2249            2 :     tl_write_uint32(&w, 0x9a8ae1e1u);
    2250            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2251            2 :     tl_write_uint32(&w, 0x98dd8936u);          /* pageListOrderedItemBlocks */
    2252            2 :     tl_write_string(&w, "a.");
    2253            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2254            2 :     tl_write_uint32(&w, 0xdb20b188u);
    2255            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2256            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2257            2 :     TlReader r = tl_reader_init(w.data, w.len);
    2258            2 :     ASSERT(tl_skip_message_media_ex(&r, NULL) == 0, "list blocks variant walked");
    2259            2 :     ASSERT(r.pos == r.len, "consumed list blocks");
    2260            2 :     tl_writer_free(&w);
    2261              : }
    2262              : 
    2263              : /* ---- Remaining rich text + other page blocks ---- */
    2264            2 : static void test_page_block_extra_variants(void) {
    2265              :     /* pageBlockSlideshow, pageBlockEmbed, pageBlockEmbedPost,
    2266              :      * pageBlockVideo, pageBlockAudio, pageBlockPhoto, pageBlockMap,
    2267              :      * pageBlockChannel — and rich text subscript/superscript/marked/fixed/strike. */
    2268            2 :     TlWriter w; tl_writer_init(&w);
    2269            2 :     tl_write_uint32(&w, 0xddf8c26eu);
    2270            2 :     tl_write_uint32(&w, 0);
    2271            2 :     tl_write_uint32(&w, 0xe89c45b2u);
    2272            2 :     tl_write_uint32(&w, (1u << 10));
    2273            2 :     tl_write_int64 (&w, 105LL);
    2274            2 :     tl_write_string(&w, "https://t.me/e"); tl_write_string(&w, "t.me");
    2275            2 :     tl_write_int32 (&w, 0);
    2276            2 :     tl_write_uint32(&w, 0x98657f0du);
    2277            2 :     tl_write_uint32(&w, 0);
    2278            2 :     tl_write_string(&w, "https://t.me/e");
    2279            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 10);
    2280              :     /* pageBlockSlideshow: blocks + caption */
    2281            2 :     tl_write_uint32(&w, 0x031f9590u);
    2282            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
    2283            2 :     tl_write_uint32(&w, 0xdb20b188u);
    2284            2 :     tl_write_uint32(&w, 0x6f747657u);          /* pageCaption */
    2285            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2286              :     /* pageBlockEmbed: flags=0 (no url, no html, no poster, no wh) + caption */
    2287            2 :     tl_write_uint32(&w, 0xa8718dc5u);
    2288            2 :     tl_write_uint32(&w, 0);
    2289            2 :     tl_write_uint32(&w, 0x6f747657u);
    2290            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2291              :     /* pageBlockEmbedPost: url + webpage_id + author_photo_id + author + date + blocks + caption */
    2292            2 :     tl_write_uint32(&w, 0xf259a80bu);
    2293            2 :     tl_write_string(&w, "https://t.me/post/1");
    2294            2 :     tl_write_int64 (&w, 8LL);
    2295            2 :     tl_write_int64 (&w, 9LL);
    2296            2 :     tl_write_string(&w, "PostAuthor");
    2297            2 :     tl_write_int32 (&w, 1700001000);
    2298            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2299            2 :     tl_write_uint32(&w, 0x6f747657u);
    2300            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2301              :     /* pageBlockVideo: flags=0 video_id + caption */
    2302            2 :     tl_write_uint32(&w, 0x7c8fe7b6u);
    2303            2 :     tl_write_uint32(&w, 0);                    /* flags */
    2304            2 :     tl_write_int64 (&w, 11LL);
    2305            2 :     tl_write_uint32(&w, 0x6f747657u);
    2306            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2307              :     /* pageBlockAudio: audio_id + caption */
    2308            2 :     tl_write_uint32(&w, 0x804361eau);
    2309            2 :     tl_write_int64 (&w, 12LL);
    2310            2 :     tl_write_uint32(&w, 0x6f747657u);
    2311            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2312              :     /* pageBlockPhoto: flags=0 photo_id + caption */
    2313            2 :     tl_write_uint32(&w, 0x1759c560u);
    2314            2 :     tl_write_uint32(&w, 0);
    2315            2 :     tl_write_int64 (&w, 13LL);
    2316            2 :     tl_write_uint32(&w, 0x6f747657u);
    2317            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2318              :     /* pageBlockMap: geoPointEmpty + zoom w h + caption */
    2319            2 :     tl_write_uint32(&w, 0xa44f3ef6u);
    2320            2 :     tl_write_uint32(&w, 0x1117dd5fu);          /* geoPointEmpty */
    2321            2 :     tl_write_int32 (&w, 10); tl_write_int32(&w, 400); tl_write_int32(&w, 300);
    2322            2 :     tl_write_uint32(&w, 0x6f747657u);
    2323            2 :     tl_write_uint32(&w, 0xdc3d824fu); tl_write_uint32(&w, 0xdc3d824fu);
    2324              :     /* pageBlockChannel: a chatEmpty */
    2325            2 :     tl_write_uint32(&w, 0xef1751b5u);
    2326            2 :     tl_write_uint32(&w, TL_chatEmpty);
    2327            2 :     tl_write_int64 (&w, 333LL);
    2328              :     /* pageBlockSubtitle: rich text with subscript + superscript + marked + fixed + strike */
    2329            2 :     tl_write_uint32(&w, 0x8ffa9a1fu);
    2330            2 :     tl_write_uint32(&w, 0x7e6260d7u);          /* textConcat */
    2331            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 5);
    2332            2 :     tl_write_uint32(&w, 0xed6a8504u); tl_write_uint32(&w, 0xdc3d824fu); /* subscript */
    2333            2 :     tl_write_uint32(&w, 0xc7fb5e01u); tl_write_uint32(&w, 0xdc3d824fu); /* superscript */
    2334            2 :     tl_write_uint32(&w, 0x034b27f6u); tl_write_uint32(&w, 0xdc3d824fu); /* marked */
    2335            2 :     tl_write_uint32(&w, 0x6c3f19b9u); tl_write_uint32(&w, 0xdc3d824fu); /* fixed */
    2336            2 :     tl_write_uint32(&w, 0x9bf8bb95u); tl_write_uint32(&w, 0xdc3d824fu); /* strike */
    2337              :     /* pageBlockKicker + pageBlockHeader + pageBlockSubheader + pageBlockFooter */
    2338            2 :     tl_write_uint32(&w, 0x1e148390u);          /* kicker */
    2339            2 :     tl_write_uint32(&w, 0xdc3d824fu);
    2340            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2341            2 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
    2342            2 :     TlReader r = tl_reader_init(w.data, w.len);
    2343            2 :     ASSERT(tl_skip_message_media_ex(&r, NULL) == 0, "extra page blocks walked");
    2344            2 :     ASSERT(r.pos == r.len, "consumed extra page blocks");
    2345            2 :     tl_writer_free(&w);
    2346              : }
    2347              : 
    2348            2 : void run_tl_forward_compat_tests(void) {
    2349            2 :     RUN_TEST(test_unknown_top_level_result_skipped);
    2350            2 :     RUN_TEST(test_unknown_media_in_history);
    2351            2 :     RUN_TEST(test_unknown_message_action);
    2352            2 :     RUN_TEST(test_unknown_optional_field_preserves_layout);
    2353            2 :     RUN_TEST(test_unknown_update_type_in_getdifference);
    2354            2 :     RUN_TEST(test_skip_primitives_reject_unknown);
    2355            2 :     RUN_TEST(test_message_entity_variants);
    2356            2 :     RUN_TEST(test_media_variants_skip_clean);
    2357            2 :     RUN_TEST(test_reply_markup_variants);
    2358            2 :     RUN_TEST(test_chat_user_unknown_variants);
    2359            2 :     RUN_TEST(test_truncation_rejected);
    2360            2 :     RUN_TEST(test_photo_and_photo_size_roundtrip);
    2361            2 :     RUN_TEST(test_document_with_attributes);
    2362            2 :     RUN_TEST(test_fwd_header_variants);
    2363            2 :     RUN_TEST(test_reply_header_variants);
    2364            2 :     RUN_TEST(test_draft_message_variants);
    2365            2 :     RUN_TEST(test_notification_sound_and_settings);
    2366            2 :     RUN_TEST(test_chat_user_visuals);
    2367            2 :     RUN_TEST(test_username_color_emoji);
    2368            2 :     RUN_TEST(test_restriction_reason_vector);
    2369            2 :     RUN_TEST(test_factcheck_variants);
    2370            2 :     RUN_TEST(test_reactions_replies_trailers);
    2371            2 :     RUN_TEST(test_media_photo_and_document);
    2372            2 :     RUN_TEST(test_message_replies_empty);
    2373            2 :     RUN_TEST(test_media_game);
    2374            2 :     RUN_TEST(test_media_invoice);
    2375            2 :     RUN_TEST(test_media_paid_media);
    2376            2 :     RUN_TEST(test_media_story);
    2377            2 :     RUN_TEST(test_media_poll_with_results);
    2378            2 :     RUN_TEST(test_media_webpage_variants);
    2379            2 :     RUN_TEST(test_chat_admin_banned_rights);
    2380            2 :     RUN_TEST(test_page_block_list_blocks_variant);
    2381            2 :     RUN_TEST(test_page_block_extra_variants);
    2382            2 : }
        

Generated by: LCOV version 2.0-1