LCOV - code coverage report
Current view: top level - src/infrastructure - media_index.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 90.1 % 81 73
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 4 4

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file infrastructure/media_index.c
       6              :  * @brief media_id → local path index stored at ~/.cache/tg-cli/media.idx
       7              :  *
       8              :  * The file is a plain-text tab-delimited table:
       9              :  *   <int64 media_id>\t<absolute path>\n
      10              :  *
      11              :  * Reads are O(n) sequential scans — the file is expected to hold at
      12              :  * most a few thousand entries in typical usage.  Writes rewrite the
      13              :  * entire file after updating the relevant entry to avoid stale lines.
      14              :  */
      15              : 
      16              : #include "media_index.h"
      17              : #include "platform/path.h"
      18              : #include "fs_util.h"
      19              : #include "logger.h"
      20              : #include "raii.h"
      21              : 
      22              : #include <stdio.h>
      23              : #include <stdlib.h>
      24              : #include <string.h>
      25              : 
      26              : #define MEDIA_IDX_MAX_LINE 2048
      27              : #define MEDIA_IDX_MAX_ENTRIES 8192
      28              : 
      29              : /** Returns a heap-allocated path to the index file.  Caller must free. */
      30           99 : static char *index_file_path(void) {
      31           99 :     const char *cache = platform_cache_dir();
      32           99 :     if (!cache) return NULL;
      33           99 :     char *p = NULL;
      34           99 :     if (asprintf(&p, "%s/tg-cli/media.idx", cache) == -1) return NULL;
      35           99 :     return p;
      36              : }
      37              : 
      38              : /** Ensure parent directory of the index file exists. */
      39           35 : static int ensure_dir(const char *index_path) {
      40           35 :     RAII_STRING char *dir = strdup(index_path);
      41           35 :     if (!dir) return -1;
      42           35 :     char *slash = strrchr(dir, '/');
      43           35 :     if (slash) *slash = '\0';
      44           35 :     return fs_mkdir_p(dir, 0700);
      45              : }
      46              : 
      47           36 : int media_index_put(int64_t media_id, const char *local_path) {
      48           36 :     if (!local_path) return -1;
      49              : 
      50           70 :     RAII_STRING char *idx = index_file_path();
      51           35 :     if (!idx) return -1;
      52           35 :     if (ensure_dir(idx) != 0) {
      53            0 :         logger_log(LOG_ERROR, "media_index_put: cannot create index directory");
      54            0 :         return -1;
      55              :     }
      56              : 
      57              :     /* Read existing entries (if any). */
      58           35 :     char (*lines)[MEDIA_IDX_MAX_LINE] = NULL;
      59           35 :     int count = 0;
      60           35 :     int found = 0;
      61              : 
      62              :     {
      63           70 :         RAII_FILE FILE *rf = fopen(idx, "r");
      64           35 :         if (rf) {
      65            7 :             lines = calloc(MEDIA_IDX_MAX_ENTRIES,
      66              :                            sizeof(*lines));
      67            7 :             if (!lines) return -1;
      68              :             char buf[MEDIA_IDX_MAX_LINE];
      69            7 :             while (count < MEDIA_IDX_MAX_ENTRIES
      70           18 :                    && fgets(buf, sizeof(buf), rf)) {
      71              :                 /* Strip trailing newline */
      72           11 :                 size_t n = strlen(buf);
      73           22 :                 while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r'))
      74           11 :                     buf[--n] = '\0';
      75           11 :                 if (n == 0) continue;
      76              : 
      77              :                 /* Check if this line is for our media_id */
      78           11 :                 int64_t id = 0;
      79           11 :                 char *tab = strchr(buf, '\t');
      80           11 :                 if (tab) {
      81           11 :                     *tab = '\0';
      82           11 :                     id = (int64_t)strtoll(buf, NULL, 10);
      83           11 :                     *tab = '\t';
      84              :                 }
      85           11 :                 if (id == media_id) {
      86              :                     /* Overwrite with new path */
      87            4 :                     snprintf(lines[count], MEDIA_IDX_MAX_LINE,
      88              :                              "%lld\t%s", (long long)media_id, local_path);
      89            4 :                     found = 1;
      90              :                 } else {
      91            7 :                     snprintf(lines[count], MEDIA_IDX_MAX_LINE, "%s", buf);
      92              :                 }
      93           11 :                 count++;
      94              :             }
      95              :         } else {
      96           28 :             lines = calloc(MEDIA_IDX_MAX_ENTRIES, sizeof(*lines));
      97           28 :             if (!lines) return -1;
      98              :         }
      99              :     }
     100              : 
     101           35 :     if (!found) {
     102           31 :         if (count < MEDIA_IDX_MAX_ENTRIES) {
     103           31 :             snprintf(lines[count], MEDIA_IDX_MAX_LINE,
     104              :                      "%lld\t%s", (long long)media_id, local_path);
     105           31 :             count++;
     106              :         } else {
     107            0 :             logger_log(LOG_WARN, "media_index_put: index full (%d entries)",
     108              :                        count);
     109            0 :             free(lines);
     110            0 :             return -1;
     111              :         }
     112              :     }
     113              : 
     114              :     /* Write all entries back. */
     115           70 :     RAII_FILE FILE *wf = fopen(idx, "w");
     116           35 :     if (!wf) {
     117            0 :         logger_log(LOG_ERROR, "media_index_put: cannot open index for writing");
     118            0 :         free(lines);
     119            0 :         return -1;
     120              :     }
     121           77 :     for (int i = 0; i < count; i++) {
     122           42 :         fprintf(wf, "%s\n", lines[i]);
     123              :     }
     124           35 :     free(lines);
     125           35 :     return 0;
     126              : }
     127              : 
     128           65 : int media_index_get(int64_t media_id, char *out_path, size_t out_cap) {
     129           65 :     if (!out_path || out_cap == 0) return -1;
     130              : 
     131          128 :     RAII_STRING char *idx = index_file_path();
     132           64 :     if (!idx) return -1;
     133              : 
     134          128 :     RAII_FILE FILE *rf = fopen(idx, "r");
     135           64 :     if (!rf) return 0;   /* file doesn't exist → not cached */
     136              : 
     137              :     char buf[MEDIA_IDX_MAX_LINE];
     138           29 :     while (fgets(buf, sizeof(buf), rf)) {
     139           27 :         char *tab = strchr(buf, '\t');
     140           27 :         if (!tab) continue;
     141           27 :         *tab = '\0';
     142           27 :         int64_t id = (int64_t)strtoll(buf, NULL, 10);
     143           27 :         if (id != media_id) { *tab = '\t'; continue; }
     144              : 
     145              :         /* Found — copy path (strip trailing newline). */
     146           18 :         char *path = tab + 1;
     147           18 :         size_t plen = strlen(path);
     148           36 :         while (plen > 0 && (path[plen-1] == '\n' || path[plen-1] == '\r'))
     149           18 :             path[--plen] = '\0';
     150              : 
     151           18 :         if (plen == 0) return 0;   /* empty path → treat as not cached */
     152           18 :         snprintf(out_path, out_cap, "%s", path);
     153           18 :         return 1;
     154              :     }
     155            2 :     return 0;
     156              : }
        

Generated by: LCOV version 2.0-1