LCOV - code coverage report
Current view: top level - src/domain/write - upload.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 84.0 % 225 189
Test Date: 2026-04-20 19:54:24 Functions: 100.0 % 10 10

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file domain/write/upload.c
       6              :  * @brief upload.saveFilePart + messages.sendMedia (US-P6-02).
       7              :  */
       8              : 
       9              : #include "domain/write/upload.h"
      10              : 
      11              : #include "app/dc_session.h"
      12              : #include "tl_serial.h"
      13              : #include "tl_registry.h"
      14              : #include "mtproto_rpc.h"
      15              : #include "crypto.h"
      16              : #include "logger.h"
      17              : #include "raii.h"
      18              : 
      19              : #include <stdio.h>
      20              : #include <stdlib.h>
      21              : #include <string.h>
      22              : #include <sys/stat.h>
      23              : 
      24              : #define CRC_upload_saveFilePart           0xb304a621u
      25              : #define CRC_upload_saveBigFilePart        0xde7b673du
      26              : #define CRC_messages_sendMedia            0x7547c966u
      27              : #define CRC_inputFile                     0xf52ff27fu
      28              : #define CRC_inputFileBig                  0xfa4f0bb5u
      29              : #define CRC_inputMediaUploadedDocument    0x5b38c6c1u
      30              : #define CRC_inputMediaUploadedPhoto       0x1e287d04u
      31              : #define CRC_documentAttributeFilename     0x15590068u
      32              : 
      33            9 : static int write_input_peer(TlWriter *w, const HistoryPeer *p) {
      34            9 :     switch (p->kind) {
      35            9 :     case HISTORY_PEER_SELF:
      36            9 :         tl_write_uint32(w, TL_inputPeerSelf); return 0;
      37            0 :     case HISTORY_PEER_USER:
      38            0 :         tl_write_uint32(w, TL_inputPeerUser);
      39            0 :         tl_write_int64 (w, p->peer_id);
      40            0 :         tl_write_int64 (w, p->access_hash); return 0;
      41            0 :     case HISTORY_PEER_CHAT:
      42            0 :         tl_write_uint32(w, TL_inputPeerChat);
      43            0 :         tl_write_int64 (w, p->peer_id); return 0;
      44            0 :     case HISTORY_PEER_CHANNEL:
      45            0 :         tl_write_uint32(w, TL_inputPeerChannel);
      46            0 :         tl_write_int64 (w, p->peer_id);
      47            0 :         tl_write_int64 (w, p->access_hash); return 0;
      48            0 :     default: return -1;
      49              :     }
      50              : }
      51              : 
      52              : /* Send one upload.saveFilePart (small) or upload.saveBigFilePart (big).
      53              :  * Returns 0 on boolTrue, -1 on error. When @p out_migrate_dc is non-NULL,
      54              :  * the server's migrate_dc hint (FILE_MIGRATE_X / NETWORK_MIGRATE_X) is
      55              :  * written there so the caller can switch DCs. */
      56           32 : static int save_part(const ApiConfig *cfg,
      57              :                       MtProtoSession *s, Transport *t,
      58              :                       int is_big,
      59              :                       int64_t file_id, int32_t part_idx, int32_t total_parts,
      60              :                       const uint8_t *bytes, size_t len,
      61              :                       int *out_migrate_dc) {
      62           32 :     if (out_migrate_dc) *out_migrate_dc = 0;
      63              : 
      64           32 :     TlWriter w; tl_writer_init(&w);
      65           32 :     if (is_big) {
      66           21 :         tl_write_uint32(&w, CRC_upload_saveBigFilePart);
      67           21 :         tl_write_int64 (&w, file_id);
      68           21 :         tl_write_int32 (&w, part_idx);
      69           21 :         tl_write_int32 (&w, total_parts);
      70           21 :         tl_write_bytes (&w, bytes, len);
      71              :     } else {
      72           11 :         tl_write_uint32(&w, CRC_upload_saveFilePart);
      73           11 :         tl_write_int64 (&w, file_id);
      74           11 :         tl_write_int32 (&w, part_idx);
      75           11 :         tl_write_bytes (&w, bytes, len);
      76              :     }
      77              : 
      78           32 :     RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
      79           32 :     if (!query) { tl_writer_free(&w); return -1; }
      80           32 :     memcpy(query, w.data, w.len);
      81           32 :     size_t qlen = w.len;
      82           32 :     tl_writer_free(&w);
      83              : 
      84           32 :     uint8_t resp[256]; size_t resp_len = 0;
      85           32 :     if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0)
      86            0 :         return -1;
      87           32 :     if (resp_len < 4) return -1;
      88           32 :     uint32_t top; memcpy(&top, resp, 4);
      89           32 :     if (top == TL_rpc_error) {
      90            2 :         RpcError perr; rpc_parse_error(resp, resp_len, &perr);
      91            2 :         if (out_migrate_dc && perr.migrate_dc > 0)
      92            2 :             *out_migrate_dc = perr.migrate_dc;
      93            2 :         logger_log(LOG_ERROR, "upload: save%sFilePart RPC %d: %s (migrate=%d)",
      94              :                    is_big ? "Big" : "",
      95              :                    perr.error_code, perr.error_msg, perr.migrate_dc);
      96            2 :         return -1;
      97              :     }
      98           30 :     if (top != TL_boolTrue) {
      99            0 :         logger_log(LOG_ERROR, "upload: unexpected save%sFilePart reply 0x%08x",
     100              :                    is_big ? "Big" : "", top);
     101            0 :         return -1;
     102              :     }
     103           30 :     return 0;
     104              : }
     105              : 
     106              : /* Upload @p fp fully on the given (s, t). Caller must have rewound @p fp.
     107              :  * On NETWORK_MIGRATE_X / FILE_MIGRATE_X at any part, writes the target DC
     108              :  * to @p out_migrate_dc and returns -1 without continuing. */
     109           11 : static int upload_all_parts(const ApiConfig *cfg,
     110              :                              MtProtoSession *s, Transport *t,
     111              :                              FILE *fp, int is_big,
     112              :                              int64_t total,
     113              :                              int64_t file_id, int32_t total_parts,
     114              :                              uint8_t *chunk,
     115              :                              int32_t *out_part_count,
     116              :                              int *out_migrate_dc) {
     117           11 :     if (out_migrate_dc) *out_migrate_dc = 0;
     118           11 :     if (fseek(fp, 0, SEEK_SET) != 0) return -1;
     119              : 
     120           11 :     int32_t part_idx = 0;
     121           11 :     int64_t done = 0;
     122           41 :     while (done < total) {
     123           32 :         int64_t want = total - done;
     124           32 :         if (want > UPLOAD_CHUNK_SIZE) want = UPLOAD_CHUNK_SIZE;
     125           32 :         size_t got = fread(chunk, 1, (size_t)want, fp);
     126           32 :         if ((int64_t)got != want) {
     127            0 :             logger_log(LOG_ERROR, "upload: short read at part %d", part_idx);
     128            2 :             return -1;
     129              :         }
     130           32 :         int migrate = 0;
     131           32 :         if (save_part(cfg, s, t, is_big, file_id, part_idx, total_parts,
     132              :                         chunk, got, &migrate) != 0) {
     133            2 :             if (out_migrate_dc && migrate > 0) *out_migrate_dc = migrate;
     134            2 :             logger_log(LOG_ERROR, "upload: part %d failed (migrate=%d)",
     135              :                        part_idx, migrate);
     136            2 :             return -1;
     137              :         }
     138           30 :         done += (int64_t)got;
     139           30 :         part_idx++;
     140              :     }
     141            9 :     if (out_part_count) *out_part_count = part_idx;
     142            9 :     return 0;
     143              : }
     144              : 
     145              : /* Extract the basename of a path, e.g. "/a/b/c.jpg" → "c.jpg". */
     146            9 : static const char *basename_of(const char *path) {
     147            9 :     const char *slash = strrchr(path, '/');
     148            9 :     return slash ? slash + 1 : path;
     149              : }
     150              : 
     151            6 : int domain_path_is_image(const char *path) {
     152            6 :     if (!path) return 0;
     153            5 :     const char *dot = strrchr(path, '.');
     154            5 :     if (!dot) return 0;
     155            4 :     const char *ext = dot + 1;
     156              :     /* Case-insensitive extension match. */
     157            4 :     char buf[8] = {0};
     158            4 :     size_t n = strlen(ext);
     159            4 :     if (n == 0 || n >= sizeof(buf)) return 0;
     160           17 :     for (size_t i = 0; i < n; i++) {
     161           13 :         unsigned c = (unsigned char)ext[i];
     162           13 :         if (c >= 'A' && c <= 'Z') c += 32;
     163           13 :         buf[i] = (char)c;
     164              :     }
     165            7 :     return (strcmp(buf, "jpg")  == 0 ||
     166            3 :             strcmp(buf, "jpeg") == 0 ||
     167            3 :             strcmp(buf, "png")  == 0 ||
     168            2 :             strcmp(buf, "webp") == 0 ||
     169            7 :             strcmp(buf, "gif")  == 0) ? 1 : 0;
     170              : }
     171              : 
     172              : /* Chunked upload step: open @p file_path, generate a random file_id,
     173              :  * push every part via upload_all_parts (with cross-DC migration), and
     174              :  * hand the resulting (file_id, part_count, is_big) back to the caller
     175              :  * so it can compose the second-phase messages.sendMedia. */
     176              : typedef struct {
     177              :     int64_t  file_id;
     178              :     int32_t  part_count;
     179              :     int      is_big;
     180              :     char     file_name[256];
     181              : } UploadedFile;
     182              : 
     183           12 : static int upload_chunk_phase(const ApiConfig *cfg,
     184              :                                MtProtoSession *s, Transport *t,
     185              :                                const char *file_path,
     186              :                                UploadedFile *uf) {
     187              :     struct stat st;
     188           12 :     if (stat(file_path, &st) != 0 || !S_ISREG(st.st_mode)) {
     189            1 :         logger_log(LOG_ERROR, "upload: cannot stat %s", file_path);
     190            1 :         return -1;
     191              :     }
     192           11 :     if (st.st_size <= 0 || (int64_t)st.st_size > UPLOAD_MAX_SIZE) {
     193            2 :         logger_log(LOG_ERROR,
     194              :                    "upload: size %lld out of supported range (1..%lld)",
     195            2 :                    (long long)st.st_size, (long long)UPLOAD_MAX_SIZE);
     196            2 :         return -1;
     197              :     }
     198            9 :     int is_big = ((int64_t)st.st_size >= UPLOAD_BIG_THRESHOLD);
     199            9 :     int64_t total = (int64_t)st.st_size;
     200            9 :     int32_t total_parts = (int32_t)((total + UPLOAD_CHUNK_SIZE - 1)
     201            9 :                                      / UPLOAD_CHUNK_SIZE);
     202              : 
     203           18 :     RAII_FILE FILE *fp = fopen(file_path, "rb");
     204            9 :     if (!fp) {
     205            0 :         logger_log(LOG_ERROR, "upload: cannot open %s", file_path);
     206            0 :         return -1;
     207              :     }
     208              : 
     209            9 :     uint8_t id_buf[8] = {0};
     210            9 :     int64_t file_id = 0;
     211            9 :     if (crypto_rand_bytes(id_buf, 8) != 0) return -1;
     212            9 :     memcpy(&file_id, id_buf, 8);
     213              : 
     214            9 :     RAII_STRING uint8_t *chunk = (uint8_t *)malloc(UPLOAD_CHUNK_SIZE);
     215            9 :     if (!chunk) return -1;
     216              : 
     217            9 :     int32_t part_idx = 0;
     218            9 :     int home_migrate = 0;
     219            9 :     if (upload_all_parts(cfg, s, t, fp, is_big, total,
     220              :                           file_id, total_parts, chunk,
     221              :                           &part_idx, &home_migrate) != 0) {
     222            2 :         if (home_migrate <= 0) {
     223            0 :             logger_log(LOG_ERROR, "upload: non-migrate failure");
     224            0 :             return -1;
     225              :         }
     226            2 :         logger_log(LOG_INFO,
     227              :                    "upload: NETWORK/FILE_MIGRATE_%d, retrying on DC%d",
     228              :                    home_migrate, home_migrate);
     229              : 
     230              :         DcSession xdc;
     231            2 :         if (dc_session_open(home_migrate, &xdc) != 0) return -1;
     232            2 :         if (dc_session_ensure_authorized(&xdc, cfg, s, t) != 0) {
     233            0 :             dc_session_close(&xdc);
     234            0 :             return -1;
     235              :         }
     236              : 
     237            2 :         uint8_t id2_buf[8] = {0};
     238            2 :         if (crypto_rand_bytes(id2_buf, 8) != 0) {
     239            0 :             dc_session_close(&xdc); return -1;
     240              :         }
     241            2 :         memcpy(&file_id, id2_buf, 8);
     242              : 
     243            2 :         int fdc_migrate = 0;
     244            2 :         int rc2 = upload_all_parts(cfg, &xdc.session, &xdc.transport, fp,
     245              :                                     is_big, total, file_id, total_parts,
     246              :                                     chunk, &part_idx, &fdc_migrate);
     247            2 :         dc_session_close(&xdc);
     248            2 :         if (rc2 != 0) {
     249            0 :             logger_log(LOG_ERROR, "upload: retry on DC%d also failed",
     250              :                        home_migrate);
     251            0 :             return -1;
     252              :         }
     253              :     }
     254              : 
     255            9 :     uf->file_id    = file_id;
     256            9 :     uf->part_count = part_idx;
     257            9 :     uf->is_big     = is_big;
     258            9 :     const char *bn = basename_of(file_path);
     259            9 :     size_t bn_n = strlen(bn);
     260            9 :     if (bn_n >= sizeof(uf->file_name)) bn_n = sizeof(uf->file_name) - 1;
     261            9 :     memcpy(uf->file_name, bn, bn_n);
     262            9 :     uf->file_name[bn_n] = '\0';
     263            9 :     return 0;
     264              : }
     265              : 
     266              : /* Serialise an InputFile / InputFileBig reference body into @p w. */
     267            9 : static void write_input_file(TlWriter *w, const UploadedFile *uf) {
     268            9 :     if (uf->is_big) {
     269            1 :         tl_write_uint32(w, CRC_inputFileBig);
     270            1 :         tl_write_int64 (w, uf->file_id);
     271            1 :         tl_write_int32 (w, uf->part_count);
     272            1 :         tl_write_string(w, uf->file_name);
     273              :     } else {
     274            8 :         tl_write_uint32(w, CRC_inputFile);
     275            8 :         tl_write_int64 (w, uf->file_id);
     276            8 :         tl_write_int32 (w, uf->part_count);
     277            8 :         tl_write_string(w, uf->file_name);
     278            8 :         tl_write_string(w, "");                   /* md5_checksum */
     279              :     }
     280            9 : }
     281              : 
     282              : /* Dispatch messages.sendMedia and drain the response. */
     283            9 : static int send_media_call(const ApiConfig *cfg,
     284              :                             MtProtoSession *s, Transport *t,
     285              :                             const uint8_t *query, size_t qlen,
     286              :                             RpcError *err) {
     287            9 :     uint8_t resp[4096]; size_t resp_len = 0;
     288            9 :     if (api_call(cfg, s, t, query, qlen, resp, sizeof(resp), &resp_len) != 0) {
     289            0 :         logger_log(LOG_ERROR, "upload: sendMedia api_call failed");
     290            0 :         return -1;
     291              :     }
     292            9 :     if (resp_len < 4) return -1;
     293            9 :     uint32_t top; memcpy(&top, resp, 4);
     294            9 :     if (top == TL_rpc_error) {
     295            0 :         if (err) rpc_parse_error(resp, resp_len, err);
     296            0 :         return -1;
     297              :     }
     298            9 :     if (top == TL_updates || top == TL_updatesCombined
     299            0 :         || top == TL_updateShort) {
     300            9 :         return 0;
     301              :     }
     302            0 :     logger_log(LOG_WARN, "upload: unexpected sendMedia top 0x%08x", top);
     303            0 :     return 0;
     304              : }
     305              : 
     306           12 : int domain_send_file(const ApiConfig *cfg,
     307              :                       MtProtoSession *s, Transport *t,
     308              :                       const HistoryPeer *peer,
     309              :                       const char *file_path,
     310              :                       const char *caption,
     311              :                       const char *mime_type,
     312              :                       RpcError *err) {
     313           12 :     if (!cfg || !s || !t || !peer || !file_path) return -1;
     314           11 :     if (!mime_type) mime_type = "application/octet-stream";
     315           11 :     if (!caption)   caption   = "";
     316              : 
     317           11 :     UploadedFile uf = {0};
     318           11 :     if (upload_chunk_phase(cfg, s, t, file_path, &uf) != 0) return -1;
     319              : 
     320            8 :     uint8_t rand_rnd[8] = {0};
     321            8 :     int64_t random_id = 0;
     322            8 :     if (crypto_rand_bytes(rand_rnd, 8) == 0) memcpy(&random_id, rand_rnd, 8);
     323              : 
     324            8 :     TlWriter w; tl_writer_init(&w);
     325            8 :     tl_write_uint32(&w, CRC_messages_sendMedia);
     326            8 :     tl_write_uint32(&w, 0);                       /* flags = 0 */
     327            8 :     if (write_input_peer(&w, peer) != 0) {
     328            0 :         tl_writer_free(&w);
     329            0 :         return -1;
     330              :     }
     331              :     /* media: InputMediaUploadedDocument */
     332            8 :     tl_write_uint32(&w, CRC_inputMediaUploadedDocument);
     333            8 :     tl_write_uint32(&w, 0);                       /* inner flags */
     334            8 :     write_input_file(&w, &uf);
     335            8 :     tl_write_string(&w, mime_type);
     336              :     /* attributes: Vector<DocumentAttribute> with a single filename. */
     337            8 :     tl_write_uint32(&w, TL_vector);
     338            8 :     tl_write_uint32(&w, 1);
     339            8 :     tl_write_uint32(&w, CRC_documentAttributeFilename);
     340            8 :     tl_write_string(&w, uf.file_name);
     341              : 
     342            8 :     tl_write_string(&w, caption);                 /* message */
     343            8 :     tl_write_int64 (&w, random_id);
     344              : 
     345            8 :     RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
     346            8 :     if (!query) { tl_writer_free(&w); return -1; }
     347            8 :     memcpy(query, w.data, w.len);
     348            8 :     size_t qlen = w.len;
     349            8 :     tl_writer_free(&w);
     350              : 
     351            8 :     return send_media_call(cfg, s, t, query, qlen, err);
     352              : }
     353              : 
     354            1 : int domain_send_photo(const ApiConfig *cfg,
     355              :                        MtProtoSession *s, Transport *t,
     356              :                        const HistoryPeer *peer,
     357              :                        const char *file_path,
     358              :                        const char *caption,
     359              :                        RpcError *err) {
     360            1 :     if (!cfg || !s || !t || !peer || !file_path) return -1;
     361            1 :     if (!caption) caption = "";
     362              : 
     363            1 :     UploadedFile uf = {0};
     364            1 :     if (upload_chunk_phase(cfg, s, t, file_path, &uf) != 0) return -1;
     365              : 
     366            1 :     uint8_t rand_rnd[8] = {0};
     367            1 :     int64_t random_id = 0;
     368            1 :     if (crypto_rand_bytes(rand_rnd, 8) == 0) memcpy(&random_id, rand_rnd, 8);
     369              : 
     370            1 :     TlWriter w; tl_writer_init(&w);
     371            1 :     tl_write_uint32(&w, CRC_messages_sendMedia);
     372            1 :     tl_write_uint32(&w, 0);                       /* outer flags = 0 */
     373            1 :     if (write_input_peer(&w, peer) != 0) {
     374            0 :         tl_writer_free(&w);
     375            0 :         return -1;
     376              :     }
     377              :     /* media: InputMediaUploadedPhoto#1e287d04 flags:# spoiler:flags.2?true
     378              :      *   file:InputFile stickers:flags.0?Vector<InputDocument>
     379              :      *   ttl_seconds:flags.1?int
     380              :      * All optional flags are clear here; server rescales. */
     381            1 :     tl_write_uint32(&w, CRC_inputMediaUploadedPhoto);
     382            1 :     tl_write_uint32(&w, 0);                       /* inner flags */
     383            1 :     write_input_file(&w, &uf);
     384              : 
     385            1 :     tl_write_string(&w, caption);
     386            1 :     tl_write_int64 (&w, random_id);
     387              : 
     388            1 :     RAII_STRING uint8_t *query = (uint8_t *)malloc(w.len);
     389            1 :     if (!query) { tl_writer_free(&w); return -1; }
     390            1 :     memcpy(query, w.data, w.len);
     391            1 :     size_t qlen = w.len;
     392            1 :     tl_writer_free(&w);
     393              : 
     394            1 :     return send_media_call(cfg, s, t, query, qlen, err);
     395              : }
        

Generated by: LCOV version 2.0-1