LCOV - code coverage report
Current view: top level - src/domain/read - media.c (source / functions) Coverage Total Hit
Test: coverage-functional.info Lines: 94.7 % 151 143
Test Date: 2026-04-20 19:54:24 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            1 :             logger_log(LOG_ERROR, "media: CDN redirect not supported");
     120            1 :             return -1;
     121              :         }
     122           15 :         if (top != CRC_upload_file) {
     123            1 :             logger_log(LOG_ERROR, "media: unexpected top 0x%08x", top);
     124            1 :             return -1;
     125              :         }
     126              : 
     127           14 :         TlReader r = tl_reader_init(resp, resp_len);
     128           14 :         tl_read_uint32(&r);                 /* top */
     129           14 :         tl_read_uint32(&r);                 /* storage.FileType crc */
     130           14 :         tl_read_int32(&r);                  /* mtime */
     131           14 :         size_t bytes_len = 0;
     132           28 :         RAII_STRING uint8_t *bytes = tl_read_bytes(&r, &bytes_len);
     133           14 :         if (!bytes && bytes_len != 0) return -1;
     134              : 
     135           14 :         if (bytes_len > 0) {
     136           14 :             if (fwrite(bytes, 1, bytes_len, fp) != bytes_len) {
     137            0 :                 logger_log(LOG_ERROR, "media: fwrite failed");
     138            0 :                 return -1;
     139              :             }
     140           14 :             offset += (int64_t)bytes_len;
     141              :         }
     142              : 
     143           14 :         if (bytes_len < CHUNK_SIZE) break;
     144              :     }
     145              : 
     146           12 :     logger_log(LOG_INFO, "media: saved %lld bytes to %s",
     147              :                (long long)offset, out_path);
     148           12 :     return 0;
     149              : }
     150              : 
     151            8 : int domain_download_photo(const ApiConfig *cfg,
     152              :                            MtProtoSession *s, Transport *t,
     153              :                            const MediaInfo *info,
     154              :                            const char *out_path,
     155              :                            int *wrong_dc) {
     156            8 :     if (wrong_dc) *wrong_dc = 0;
     157            8 :     if (!cfg || !s || !t || !info || !out_path) return -1;
     158            8 :     if (info->kind != MEDIA_PHOTO) {
     159            1 :         logger_log(LOG_ERROR, "media: download_photo needs MEDIA_PHOTO");
     160            1 :         return -1;
     161              :     }
     162            7 :     if (info->photo_id == 0 || info->access_hash == 0
     163            7 :         || info->file_reference_len == 0) {
     164            0 :         logger_log(LOG_ERROR, "media: missing id / access_hash / file_reference");
     165            0 :         return -1;
     166              :     }
     167              : 
     168              :     /* Cache hit: if the file is already indexed and still exists on disk,
     169              :      * copy/use the cached path rather than issuing upload.getFile again. */
     170              :     char cached[4096];
     171            7 :     if (media_index_get(info->photo_id, cached, sizeof(cached)) == 1) {
     172            2 :         FILE *fp = fopen(cached, "rb");
     173            2 :         if (fp) {
     174            2 :             fclose(fp);
     175              :             /* If the caller wants the same path that is already cached,
     176              :              * we're done.  Otherwise copy to out_path so the caller can
     177              :              * rely on it being at the requested location. */
     178            2 :             if (strcmp(cached, out_path) != 0) {
     179            2 :                 RAII_FILE FILE *src = fopen(cached, "rb");
     180            2 :                 RAII_FILE FILE *dst = fopen(out_path, "wb");
     181            1 :                 if (src && dst) {
     182              :                     uint8_t buf[4096];
     183              :                     size_t n;
     184            2 :                     while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
     185            1 :                         fwrite(buf, 1, n, dst);
     186              :                 }
     187              :             }
     188            2 :             logger_log(LOG_INFO, "media: cache hit for photo_id %lld → %s",
     189            2 :                        (long long)info->photo_id, cached);
     190            2 :             return 0;
     191              :         }
     192              :     }
     193              : 
     194            5 :     int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
     195            5 :     if (rc == 0)
     196            4 :         media_index_put(info->photo_id, out_path);
     197            5 :     return rc;
     198              : }
     199              : 
     200           20 : int domain_download_document(const ApiConfig *cfg,
     201              :                               MtProtoSession *s, Transport *t,
     202              :                               const MediaInfo *info,
     203              :                               const char *out_path,
     204              :                               int *wrong_dc) {
     205           20 :     if (wrong_dc) *wrong_dc = 0;
     206           20 :     if (!cfg || !s || !t || !info || !out_path) return -1;
     207           20 :     if (info->kind != MEDIA_DOCUMENT) {
     208            1 :         logger_log(LOG_ERROR, "media: download_document needs MEDIA_DOCUMENT");
     209            1 :         return -1;
     210              :     }
     211           19 :     if (info->document_id == 0 || info->access_hash == 0
     212           18 :         || info->file_reference_len == 0) {
     213            1 :         logger_log(LOG_ERROR,
     214              :                    "media: document missing id / access_hash / file_reference");
     215            1 :         return -1;
     216              :     }
     217              : 
     218              :     /* Cache hit: avoid re-downloading an already cached document. */
     219              :     char cached[4096];
     220           18 :     if (media_index_get(info->document_id, cached, sizeof(cached)) == 1) {
     221            2 :         FILE *fp = fopen(cached, "rb");
     222            2 :         if (fp) {
     223            2 :             fclose(fp);
     224            2 :             if (strcmp(cached, out_path) != 0) {
     225            2 :                 RAII_FILE FILE *src = fopen(cached, "rb");
     226            2 :                 RAII_FILE FILE *dst = fopen(out_path, "wb");
     227            1 :                 if (src && dst) {
     228              :                     uint8_t buf[4096];
     229              :                     size_t n;
     230            2 :                     while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
     231            1 :                         fwrite(buf, 1, n, dst);
     232              :                 }
     233              :             }
     234            2 :             logger_log(LOG_INFO, "media: cache hit for document_id %lld → %s",
     235            2 :                        (long long)info->document_id, cached);
     236            2 :             return 0;
     237              :         }
     238              :     }
     239              : 
     240           16 :     int rc = download_loop(cfg, s, t, info, out_path, wrong_dc);
     241           16 :     if (rc == 0)
     242            8 :         media_index_put(info->document_id, out_path);
     243           16 :     return rc;
     244              : }
     245              : 
     246              : /* Dispatch on MediaKind and call the right per-type entry point so the
     247              :  * cross-DC wrapper does not need to know the argument validation rules. */
     248            5 : static int download_any(const ApiConfig *cfg,
     249              :                          MtProtoSession *s, Transport *t,
     250              :                          const MediaInfo *info,
     251              :                          const char *out_path,
     252              :                          int *wrong_dc) {
     253            5 :     if (info->kind == MEDIA_PHOTO)
     254            0 :         return domain_download_photo(cfg, s, t, info, out_path, wrong_dc);
     255            5 :     if (info->kind == MEDIA_DOCUMENT)
     256            4 :         return domain_download_document(cfg, s, t, info, out_path, wrong_dc);
     257            1 :     logger_log(LOG_ERROR, "media: download_any unsupported kind=%d", info->kind);
     258            1 :     return -1;
     259              : }
     260              : 
     261            4 : int domain_download_media_cross_dc(const ApiConfig *cfg,
     262              :                                     MtProtoSession *home_s, Transport *home_t,
     263              :                                     const MediaInfo *info,
     264              :                                     const char *out_path) {
     265            4 :     if (!cfg || !home_s || !home_t || !info || !out_path) return -1;
     266              : 
     267            4 :     int wrong_dc = 0;
     268            4 :     if (download_any(cfg, home_s, home_t, info, out_path, &wrong_dc) == 0) {
     269            1 :         return 0;                                /* home DC had the file */
     270              :     }
     271            3 :     if (wrong_dc <= 0) return -1;                /* not a migration — hard fail */
     272              : 
     273            2 :     logger_log(LOG_INFO, "media: FILE_MIGRATE_%d, retrying on DC%d",
     274              :                wrong_dc, wrong_dc);
     275              : 
     276              :     DcSession xdc;
     277            2 :     if (dc_session_open(wrong_dc, &xdc) != 0) {
     278            1 :         logger_log(LOG_ERROR, "media: cannot open DC%d session", wrong_dc);
     279            1 :         return -1;
     280              :     }
     281              : 
     282              :     /* Freshly handshaked foreign sessions are not yet authorized.
     283              :      * dc_session_ensure_authorized() runs export/import; on a cached
     284              :      * session it is a no-op. */
     285            1 :     if (dc_session_ensure_authorized(&xdc, cfg, home_s, home_t) != 0) {
     286            0 :         logger_log(LOG_ERROR,
     287              :                    "media: cross-DC authorization setup failed for DC%d",
     288              :                    wrong_dc);
     289            0 :         dc_session_close(&xdc);
     290            0 :         return -1;
     291              :     }
     292              : 
     293            1 :     int dummy = 0;
     294            1 :     int rc = download_any(cfg, &xdc.session, &xdc.transport,
     295              :                           info, out_path, &dummy);
     296            1 :     if (rc != 0) {
     297            1 :         logger_log(LOG_ERROR, "media: retry on DC%d still failed", wrong_dc);
     298              :     }
     299            1 :     dc_session_close(&xdc);
     300            1 :     return rc;
     301              : }
        

Generated by: LCOV version 2.0-1