LCOV - code coverage report
Current view: top level - src/domain/read - media.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 96.8 % 154 149
Test Date: 2026-05-06 13:17:06 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           51 : 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           51 :     tl_writer_init(&w);
      41              : 
      42           51 :     tl_write_uint32(&w, CRC_upload_getFile);
      43           51 :     tl_write_uint32(&w, 0);                             /* flags */
      44           51 :     if (info->kind == MEDIA_DOCUMENT) {
      45              :         /* inputDocumentFileLocation#bad07584 id:long access_hash:long
      46              :          *                                    file_reference:bytes thumb_size:string */
      47           35 :         tl_write_uint32(&w, CRC_inputDocumentFileLocation);
      48           35 :         tl_write_int64 (&w, info->document_id);
      49           35 :         tl_write_int64 (&w, info->access_hash);
      50           35 :         tl_write_bytes (&w, info->file_reference,
      51           35 :                             info->file_reference_len);
      52           35 :         tl_write_string(&w, "");                        /* full file */
      53              :     } else {
      54              :         /* inputPhotoFileLocation#40181ffe id:long access_hash:long
      55              :          *                                 file_reference:bytes thumb_size:string */
      56           16 :         tl_write_uint32(&w, CRC_inputPhotoFileLocation);
      57           16 :         tl_write_int64 (&w, info->photo_id);
      58           16 :         tl_write_int64 (&w, info->access_hash);
      59           16 :         tl_write_bytes (&w, info->file_reference,
      60           16 :                             info->file_reference_len);
      61           16 :         tl_write_string(&w, info->thumb_type[0] ? info->thumb_type : "y");
      62              :     }
      63           51 :     tl_write_int64 (&w, offset);
      64           51 :     tl_write_int32 (&w, limit);
      65              : 
      66           51 :     int rc = -1;
      67           51 :     if (w.len <= cap) {
      68           51 :         memcpy(out, w.data, w.len);
      69           51 :         *out_len = w.len;
      70           51 :         rc = 0;
      71              :     }
      72           51 :     tl_writer_free(&w);
      73           51 :     return rc;
      74              : }
      75              : 
      76              : /* Shared chunked download loop. Caller has already validated @p info. */
      77           49 : 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           98 :     RAII_FILE FILE *fp = fopen(out_path, "wb");
      83           49 :     if (!fp) {
      84            2 :         logger_log(LOG_ERROR, "media: cannot open %s for writing", out_path);
      85            2 :         return -1;
      86              :     }
      87              : 
      88              :     uint8_t query[1024];
      89           47 :     RAII_STRING uint8_t *resp = (uint8_t *)malloc(CHUNK_SIZE + 4096);
      90           47 :     if (!resp) return -1;
      91              : 
      92           47 :     int64_t offset = 0;
      93            4 :     for (;;) {
      94           51 :         size_t qlen = 0;
      95           51 :         if (build_getfile_request(info, offset, CHUNK_SIZE,
      96              :                                     query, sizeof(query), &qlen) != 0)
      97           18 :             return -1;
      98              : 
      99           51 :         size_t resp_len = 0;
     100           51 :         if (api_call(cfg, s, t, query, qlen,
     101              :                      resp, CHUNK_SIZE + 4096, &resp_len) != 0) {
     102            2 :             logger_log(LOG_ERROR, "media: api_call failed at offset %lld",
     103              :                        (long long)offset);
     104            2 :             return -1;
     105              :         }
     106           49 :         if (resp_len < 4) return -1;
     107              : 
     108              :         uint32_t top;
     109           49 :         memcpy(&top, resp, 4);
     110           49 :         if (top == TL_rpc_error) {
     111              :             RpcError err;
     112           12 :             rpc_parse_error(resp, resp_len, &err);
     113           12 :             if (err.migrate_dc > 0 && wrong_dc) *wrong_dc = err.migrate_dc;
     114           12 :             logger_log(LOG_ERROR, "media: RPC error %d: %s (migrate=%d)",
     115              :                        err.error_code, err.error_msg, err.migrate_dc);
     116           12 :             return -1;
     117              :         }
     118           37 :         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            2 :             TlReader cr = tl_reader_init(resp, resp_len);
     125            2 :             tl_read_uint32(&cr);                    /* crc */
     126            2 :             int32_t cdn_dc = tl_reader_ok(&cr) ? tl_read_int32(&cr) : 0;
     127            2 :             logger_log(LOG_WARN,
     128              :                        "media: CDN redirect to DC%d — CDN download not implemented",
     129              :                        (int)cdn_dc);
     130            2 :             return -1;
     131              :         }
     132           35 :         if (top != CRC_upload_file) {
     133            2 :             logger_log(LOG_ERROR, "media: unexpected top 0x%08x", top);
     134            2 :             return -1;
     135              :         }
     136              : 
     137           33 :         TlReader r = tl_reader_init(resp, resp_len);
     138           33 :         tl_read_uint32(&r);                 /* top */
     139           33 :         tl_read_uint32(&r);                 /* storage.FileType crc */
     140           33 :         tl_read_int32(&r);                  /* mtime */
     141           33 :         size_t bytes_len = 0;
     142           66 :         RAII_STRING uint8_t *bytes = tl_read_bytes(&r, &bytes_len);
     143           33 :         if (!bytes && bytes_len != 0) return -1;
     144              : 
     145           33 :         if (bytes_len > 0) {
     146           33 :             if (fwrite(bytes, 1, bytes_len, fp) != bytes_len) {
     147            0 :                 logger_log(LOG_ERROR, "media: fwrite failed");
     148            0 :                 return -1;
     149              :             }
     150           33 :             offset += (int64_t)bytes_len;
     151              :         }
     152              : 
     153           33 :         if (bytes_len < CHUNK_SIZE) break;
     154              :     }
     155              : 
     156           29 :     logger_log(LOG_INFO, "media: saved %lld bytes to %s",
     157              :                (long long)offset, out_path);
     158           29 :     return 0;
     159              : }
     160              : 
     161           23 : 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           23 :     if (wrong_dc) *wrong_dc = 0;
     167           23 :     if (!cfg || !s || !t || !info || !out_path) return -1;
     168           22 :     if (info->kind != MEDIA_PHOTO) {
     169            3 :         logger_log(LOG_ERROR, "media: download_photo needs MEDIA_PHOTO");
     170            3 :         return -1;
     171              :     }
     172           19 :     if (info->photo_id == 0 || info->access_hash == 0
     173           18 :         || info->file_reference_len == 0) {
     174            1 :         logger_log(LOG_ERROR, "media: missing id / access_hash / file_reference");
     175            1 :         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           18 :     if (media_index_get(info->photo_id, cached, sizeof(cached)) == 1) {
     182            7 :         FILE *fp = fopen(cached, "rb");
     183            7 :         if (fp) {
     184            4 :             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            4 :             if (strcmp(cached, out_path) != 0) {
     189            4 :                 RAII_FILE FILE *src = fopen(cached, "rb");
     190            4 :                 RAII_FILE FILE *dst = fopen(out_path, "wb");
     191            2 :                 if (src && dst) {
     192              :                     uint8_t buf[4096];
     193              :                     size_t n;
     194            4 :                     while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
     195            2 :                         fwrite(buf, 1, n, dst);
     196              :                 }
     197              :             }
     198            4 :             logger_log(LOG_INFO, "media: cache hit for photo_id %lld → %s",
     199            4 :                        (long long)info->photo_id, cached);
     200            4 :             return 0;
     201              :         }
     202              :     }
     203              : 
     204           14 :     int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
     205           14 :     if (rc == 0)
     206           10 :         media_index_put(info->photo_id, out_path);
     207           14 :     return rc;
     208              : }
     209              : 
     210           44 : 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           44 :     if (wrong_dc) *wrong_dc = 0;
     216           44 :     if (!cfg || !s || !t || !info || !out_path) return -1;
     217           44 :     if (info->kind != MEDIA_DOCUMENT) {
     218            3 :         logger_log(LOG_ERROR, "media: download_document needs MEDIA_DOCUMENT");
     219            3 :         return -1;
     220              :     }
     221           41 :     if (info->document_id == 0 || info->access_hash == 0
     222           39 :         || info->file_reference_len == 0) {
     223            2 :         logger_log(LOG_ERROR,
     224              :                    "media: document missing id / access_hash / file_reference");
     225            2 :         return -1;
     226              :     }
     227              : 
     228              :     /* Cache hit: avoid re-downloading an already cached document. */
     229              :     char cached[4096];
     230           39 :     if (media_index_get(info->document_id, cached, sizeof(cached)) == 1) {
     231            6 :         FILE *fp = fopen(cached, "rb");
     232            6 :         if (fp) {
     233            4 :             fclose(fp);
     234            4 :             if (strcmp(cached, out_path) != 0) {
     235            4 :                 RAII_FILE FILE *src = fopen(cached, "rb");
     236            4 :                 RAII_FILE FILE *dst = fopen(out_path, "wb");
     237            2 :                 if (src && dst) {
     238              :                     uint8_t buf[4096];
     239              :                     size_t n;
     240            4 :                     while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
     241            2 :                         fwrite(buf, 1, n, dst);
     242              :                 }
     243              :             }
     244            4 :             logger_log(LOG_INFO, "media: cache hit for document_id %lld → %s",
     245            4 :                        (long long)info->document_id, cached);
     246            4 :             return 0;
     247              :         }
     248              :     }
     249              : 
     250           35 :     int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
     251           35 :     if (rc == 0)
     252           19 :         media_index_put(info->document_id, out_path);
     253           35 :     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           12 : 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           12 :     if (info->kind == MEDIA_PHOTO)
     264            2 :         return domain_download_photo(cfg, s, t, info, out_path, wrong_dc);
     265           10 :     if (info->kind == MEDIA_DOCUMENT)
     266            8 :         return domain_download_document(cfg, s, t, info, out_path, wrong_dc);
     267            2 :     logger_log(LOG_ERROR, "media: download_any unsupported kind=%d", info->kind);
     268            2 :     return -1;
     269              : }
     270              : 
     271           15 : 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           15 :     if (!cfg || !home_s || !home_t || !info || !out_path) return -1;
     276              : 
     277           10 :     int wrong_dc = 0;
     278           10 :     if (download_any(cfg, home_s, home_t, info, out_path, &wrong_dc) == 0) {
     279            3 :         return 0;                                /* home DC had the file */
     280              :     }
     281            7 :     if (wrong_dc <= 0) return -1;                /* not a migration — hard fail */
     282              : 
     283            4 :     logger_log(LOG_INFO, "media: FILE_MIGRATE_%d, retrying on DC%d",
     284              :                wrong_dc, wrong_dc);
     285              : 
     286              :     DcSession xdc;
     287            4 :     if (dc_session_open(wrong_dc, &xdc) != 0) {
     288            2 :         logger_log(LOG_ERROR, "media: cannot open DC%d session", wrong_dc);
     289            2 :         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            2 :     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            2 :     int dummy = 0;
     304            2 :     int rc = download_any(cfg, &xdc.session, &xdc.transport,
     305              :                           info, out_path, &dummy);
     306            2 :     if (rc != 0) {
     307            2 :         logger_log(LOG_ERROR, "media: retry on DC%d still failed", wrong_dc);
     308              :     }
     309            2 :     dc_session_close(&xdc);
     310            2 :     return rc;
     311              : }
        

Generated by: LCOV version 2.0-1