LCOV - code coverage report
Current view: top level - src/domain/read - media.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 94.8 % 154 146
Test Date: 2026-05-06 13:17:08 Functions: 100.0 % 6 6

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file domain/read/media.c
       6              :  * @brief upload.getFile chunked download (P6-01).
       7              :  */
       8              : 
       9              : #include "domain/read/media.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 "media_index.h"
      16              : #include "logger.h"
      17              : #include "raii.h"
      18              : 
      19              : #include <stdio.h>
      20              : #include <stdlib.h>
      21              : #include <string.h>
      22              : 
      23              : #define CRC_upload_getFile                 0xbe5335beu
      24              : #define CRC_inputPhotoFileLocation         0x40181ffeu
      25              : #define CRC_inputDocumentFileLocation      0xbad07584u
      26              : #define CRC_upload_file                    0x096a18d5u
      27              : #define CRC_upload_fileCdnRedirect         0xf18cda44u
      28              : 
      29              : /** Default chunk size — must be a multiple of 4 KB per Telegram spec. */
      30              : #define CHUNK_SIZE (128 * 1024)
      31              : 
      32              : /* Build a upload.getFile request whose InputFileLocation is derived
      33              :  * from @p info.kind. Photos use inputPhotoFileLocation with the
      34              :  * largest thumb_size; documents use inputDocumentFileLocation with
      35              :  * an empty thumb_size to fetch the full file. */
      36           22 : static int build_getfile_request(const MediaInfo *info,
      37              :                                   int64_t offset, int32_t limit,
      38              :                                   uint8_t *out, size_t cap, size_t *out_len) {
      39              :     TlWriter w;
      40           22 :     tl_writer_init(&w);
      41              : 
      42           22 :     tl_write_uint32(&w, CRC_upload_getFile);
      43           22 :     tl_write_uint32(&w, 0);                             /* flags */
      44           22 :     if (info->kind == MEDIA_DOCUMENT) {
      45              :         /* inputDocumentFileLocation#bad07584 id:long access_hash:long
      46              :          *                                    file_reference:bytes thumb_size:string */
      47           16 :         tl_write_uint32(&w, CRC_inputDocumentFileLocation);
      48           16 :         tl_write_int64 (&w, info->document_id);
      49           16 :         tl_write_int64 (&w, info->access_hash);
      50           16 :         tl_write_bytes (&w, info->file_reference,
      51           16 :                             info->file_reference_len);
      52           16 :         tl_write_string(&w, "");                        /* full file */
      53              :     } else {
      54              :         /* inputPhotoFileLocation#40181ffe id:long access_hash:long
      55              :          *                                 file_reference:bytes thumb_size:string */
      56            6 :         tl_write_uint32(&w, CRC_inputPhotoFileLocation);
      57            6 :         tl_write_int64 (&w, info->photo_id);
      58            6 :         tl_write_int64 (&w, info->access_hash);
      59            6 :         tl_write_bytes (&w, info->file_reference,
      60            6 :                             info->file_reference_len);
      61            6 :         tl_write_string(&w, info->thumb_type[0] ? info->thumb_type : "y");
      62              :     }
      63           22 :     tl_write_int64 (&w, offset);
      64           22 :     tl_write_int32 (&w, limit);
      65              : 
      66           22 :     int rc = -1;
      67           22 :     if (w.len <= cap) {
      68           22 :         memcpy(out, w.data, w.len);
      69           22 :         *out_len = w.len;
      70           22 :         rc = 0;
      71              :     }
      72           22 :     tl_writer_free(&w);
      73           22 :     return rc;
      74              : }
      75              : 
      76              : /* Shared chunked download loop. Caller has already validated @p info. */
      77           21 : static int download_loop(const ApiConfig *cfg,
      78              :                           MtProtoSession *s, Transport *t,
      79              :                           const MediaInfo *info,
      80              :                           const char *out_path,
      81              :                           int *wrong_dc) {
      82           42 :     RAII_FILE FILE *fp = fopen(out_path, "wb");
      83           21 :     if (!fp) {
      84            1 :         logger_log(LOG_ERROR, "media: cannot open %s for writing", out_path);
      85            1 :         return -1;
      86              :     }
      87              : 
      88              :     uint8_t query[1024];
      89           20 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(CHUNK_SIZE + 4096);
      90           20 :     if (!resp) return -1;
      91              : 
      92           20 :     int64_t offset = 0;
      93            2 :     for (;;) {
      94           22 :         size_t qlen = 0;
      95           22 :         if (build_getfile_request(info, offset, CHUNK_SIZE,
      96              :                                     query, sizeof(query), &qlen) != 0)
      97            8 :             return -1;
      98              : 
      99           22 :         size_t resp_len = 0;
     100           22 :         if (api_call(cfg, s, t, query, qlen,
     101              :                      resp, CHUNK_SIZE + 4096, &resp_len) != 0) {
     102            1 :             logger_log(LOG_ERROR, "media: api_call failed at offset %lld",
     103              :                        (long long)offset);
     104            1 :             return -1;
     105              :         }
     106           21 :         if (resp_len < 4) return -1;
     107              : 
     108              :         uint32_t top;
     109           21 :         memcpy(&top, resp, 4);
     110           21 :         if (top == TL_rpc_error) {
     111              :             RpcError err;
     112            5 :             rpc_parse_error(resp, resp_len, &err);
     113            5 :             if (err.migrate_dc > 0 && wrong_dc) *wrong_dc = err.migrate_dc;
     114            5 :             logger_log(LOG_ERROR, "media: RPC error %d: %s (migrate=%d)",
     115              :                        err.error_code, err.error_msg, err.migrate_dc);
     116            5 :             return -1;
     117              :         }
     118           16 :         if (top == CRC_upload_fileCdnRedirect) {
     119              :             /* upload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes
     120              :              * encryption_key:bytes encryption_iv:bytes
     121              :              * file_hashes:Vector<FileHash>
     122              :              * CDN DCs use a separate auth key and AES-256-CTR per-chunk
     123              :              * decryption — not yet implemented. */
     124            1 :             TlReader cr = tl_reader_init(resp, resp_len);
     125            1 :             tl_read_uint32(&cr);                    /* crc */
     126            1 :             int32_t cdn_dc = tl_reader_ok(&cr) ? tl_read_int32(&cr) : 0;
     127            1 :             logger_log(LOG_WARN,
     128              :                        "media: CDN redirect to DC%d — CDN download not implemented",
     129              :                        (int)cdn_dc);
     130            1 :             return -1;
     131              :         }
     132           15 :         if (top != CRC_upload_file) {
     133            1 :             logger_log(LOG_ERROR, "media: unexpected top 0x%08x", top);
     134            1 :             return -1;
     135              :         }
     136              : 
     137           14 :         TlReader r = tl_reader_init(resp, resp_len);
     138           14 :         tl_read_uint32(&r);                 /* top */
     139           14 :         tl_read_uint32(&r);                 /* storage.FileType crc */
     140           14 :         tl_read_int32(&r);                  /* mtime */
     141           14 :         size_t bytes_len = 0;
     142           28 :         RAII_STRING uint8_t *bytes = tl_read_bytes(&r, &bytes_len);
     143           14 :         if (!bytes && bytes_len != 0) return -1;
     144              : 
     145           14 :         if (bytes_len > 0) {
     146           14 :             if (fwrite(bytes, 1, bytes_len, fp) != bytes_len) {
     147            0 :                 logger_log(LOG_ERROR, "media: fwrite failed");
     148            0 :                 return -1;
     149              :             }
     150           14 :             offset += (int64_t)bytes_len;
     151              :         }
     152              : 
     153           14 :         if (bytes_len < CHUNK_SIZE) break;
     154              :     }
     155              : 
     156           12 :     logger_log(LOG_INFO, "media: saved %lld bytes to %s",
     157              :                (long long)offset, out_path);
     158           12 :     return 0;
     159              : }
     160              : 
     161            8 : int domain_download_photo(const ApiConfig *cfg,
     162              :                            MtProtoSession *s, Transport *t,
     163              :                            const MediaInfo *info,
     164              :                            const char *out_path,
     165              :                            int *wrong_dc) {
     166            8 :     if (wrong_dc) *wrong_dc = 0;
     167            8 :     if (!cfg || !s || !t || !info || !out_path) return -1;
     168            8 :     if (info->kind != MEDIA_PHOTO) {
     169            1 :         logger_log(LOG_ERROR, "media: download_photo needs MEDIA_PHOTO");
     170            1 :         return -1;
     171              :     }
     172            7 :     if (info->photo_id == 0 || info->access_hash == 0
     173            7 :         || info->file_reference_len == 0) {
     174            0 :         logger_log(LOG_ERROR, "media: missing id / access_hash / file_reference");
     175            0 :         return -1;
     176              :     }
     177              : 
     178              :     /* Cache hit: if the file is already indexed and still exists on disk,
     179              :      * copy/use the cached path rather than issuing upload.getFile again. */
     180              :     char cached[4096];
     181            7 :     if (media_index_get(info->photo_id, cached, sizeof(cached)) == 1) {
     182            2 :         FILE *fp = fopen(cached, "rb");
     183            2 :         if (fp) {
     184            2 :             fclose(fp);
     185              :             /* If the caller wants the same path that is already cached,
     186              :              * we're done.  Otherwise copy to out_path so the caller can
     187              :              * rely on it being at the requested location. */
     188            2 :             if (strcmp(cached, out_path) != 0) {
     189            2 :                 RAII_FILE FILE *src = fopen(cached, "rb");
     190            2 :                 RAII_FILE FILE *dst = fopen(out_path, "wb");
     191            1 :                 if (src && dst) {
     192              :                     uint8_t buf[4096];
     193              :                     size_t n;
     194            2 :                     while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
     195            1 :                         fwrite(buf, 1, n, dst);
     196              :                 }
     197              :             }
     198            2 :             logger_log(LOG_INFO, "media: cache hit for photo_id %lld → %s",
     199            2 :                        (long long)info->photo_id, cached);
     200            2 :             return 0;
     201              :         }
     202              :     }
     203              : 
     204            5 :     int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
     205            5 :     if (rc == 0)
     206            4 :         media_index_put(info->photo_id, out_path);
     207            5 :     return rc;
     208              : }
     209              : 
     210           20 : int domain_download_document(const ApiConfig *cfg,
     211              :                               MtProtoSession *s, Transport *t,
     212              :                               const MediaInfo *info,
     213              :                               const char *out_path,
     214              :                               int *wrong_dc) {
     215           20 :     if (wrong_dc) *wrong_dc = 0;
     216           20 :     if (!cfg || !s || !t || !info || !out_path) return -1;
     217           20 :     if (info->kind != MEDIA_DOCUMENT) {
     218            1 :         logger_log(LOG_ERROR, "media: download_document needs MEDIA_DOCUMENT");
     219            1 :         return -1;
     220              :     }
     221           19 :     if (info->document_id == 0 || info->access_hash == 0
     222           18 :         || info->file_reference_len == 0) {
     223            1 :         logger_log(LOG_ERROR,
     224              :                    "media: document missing id / access_hash / file_reference");
     225            1 :         return -1;
     226              :     }
     227              : 
     228              :     /* Cache hit: avoid re-downloading an already cached document. */
     229              :     char cached[4096];
     230           18 :     if (media_index_get(info->document_id, cached, sizeof(cached)) == 1) {
     231            2 :         FILE *fp = fopen(cached, "rb");
     232            2 :         if (fp) {
     233            2 :             fclose(fp);
     234            2 :             if (strcmp(cached, out_path) != 0) {
     235            2 :                 RAII_FILE FILE *src = fopen(cached, "rb");
     236            2 :                 RAII_FILE FILE *dst = fopen(out_path, "wb");
     237            1 :                 if (src && dst) {
     238              :                     uint8_t buf[4096];
     239              :                     size_t n;
     240            2 :                     while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
     241            1 :                         fwrite(buf, 1, n, dst);
     242              :                 }
     243              :             }
     244            2 :             logger_log(LOG_INFO, "media: cache hit for document_id %lld → %s",
     245            2 :                        (long long)info->document_id, cached);
     246            2 :             return 0;
     247              :         }
     248              :     }
     249              : 
     250           16 :     int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
     251           16 :     if (rc == 0)
     252            8 :         media_index_put(info->document_id, out_path);
     253           16 :     return rc;
     254              : }
     255              : 
     256              : /* Dispatch on MediaKind and call the right per-type entry point so the
     257              :  * cross-DC wrapper does not need to know the argument validation rules. */
     258            5 : static int download_any(const ApiConfig *cfg,
     259              :                          MtProtoSession *s, Transport *t,
     260              :                          const MediaInfo *info,
     261              :                          const char *out_path,
     262              :                          int *wrong_dc) {
     263            5 :     if (info->kind == MEDIA_PHOTO)
     264            0 :         return domain_download_photo(cfg, s, t, info, out_path, wrong_dc);
     265            5 :     if (info->kind == MEDIA_DOCUMENT)
     266            4 :         return domain_download_document(cfg, s, t, info, out_path, wrong_dc);
     267            1 :     logger_log(LOG_ERROR, "media: download_any unsupported kind=%d", info->kind);
     268            1 :     return -1;
     269              : }
     270              : 
     271            4 : int domain_download_media_cross_dc(const ApiConfig *cfg,
     272              :                                     MtProtoSession *home_s, Transport *home_t,
     273              :                                     const MediaInfo *info,
     274              :                                     const char *out_path) {
     275            4 :     if (!cfg || !home_s || !home_t || !info || !out_path) return -1;
     276              : 
     277            4 :     int wrong_dc = 0;
     278            4 :     if (download_any(cfg, home_s, home_t, info, out_path, &wrong_dc) == 0) {
     279            1 :         return 0;                                /* home DC had the file */
     280              :     }
     281            3 :     if (wrong_dc <= 0) return -1;                /* not a migration — hard fail */
     282              : 
     283            2 :     logger_log(LOG_INFO, "media: FILE_MIGRATE_%d, retrying on DC%d",
     284              :                wrong_dc, wrong_dc);
     285              : 
     286              :     DcSession xdc;
     287            2 :     if (dc_session_open(wrong_dc, &xdc) != 0) {
     288            1 :         logger_log(LOG_ERROR, "media: cannot open DC%d session", wrong_dc);
     289            1 :         return -1;
     290              :     }
     291              : 
     292              :     /* Freshly handshaked foreign sessions are not yet authorized.
     293              :      * dc_session_ensure_authorized() runs export/import; on a cached
     294              :      * session it is a no-op. */
     295            1 :     if (dc_session_ensure_authorized(&xdc, cfg, home_s, home_t) != 0) {
     296            0 :         logger_log(LOG_ERROR,
     297              :                    "media: cross-DC authorization setup failed for DC%d",
     298              :                    wrong_dc);
     299            0 :         dc_session_close(&xdc);
     300            0 :         return -1;
     301              :     }
     302              : 
     303            1 :     int dummy = 0;
     304            1 :     int rc = download_any(cfg, &xdc.session, &xdc.transport,
     305              :                           info, out_path, &dummy);
     306            1 :     if (rc != 0) {
     307            1 :         logger_log(LOG_ERROR, "media: retry on DC%d still failed", wrong_dc);
     308              :     }
     309            1 :     dc_session_close(&xdc);
     310            1 :     return rc;
     311              : }
        

Generated by: LCOV version 2.0-1