ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
downloads.c
Go to the documentation of this file.
1#include "mcp.h"
2#include <uv.h>
3#include <ctype.h>
4#include <stdlib.h>
5#include <time.h>
6
7const char *const mcp_output_formats[] = {"png", "jpg", "jpeg", "bmp", "webp", "gif", "mp4", "mov",
8 "mkv", "webm", "avi", "flv", "ogv", "ogg", "3gp", NULL};
9
10const char *mcp_output_mime(const char *extension) {
11 static const char *const mime[] = {"image/png", "image/jpeg", "image/jpeg", "image/bmp",
12 "image/webp", "image/gif", "video/mp4", "video/quicktime",
13 "video/x-matroska", "video/webm", "video/x-msvideo", "video/x-flv",
14 "video/ogg", "video/ogg", "video/3gpp"};
15 for (size_t i = 0; mcp_output_formats[i]; ++i)
16 if (!strcmp(extension, mcp_output_formats[i]))
17 return mime[i];
18 return NULL;
19}
20
21// Filenames are generated internally; strict parsing also prevents path traversal.
22bool mcp_download_name(const char *name, int64_t *expires) {
23 if (!name || strlen(name) < 47)
24 return false;
25 const char *p = name;
26 while (isdigit((unsigned char)*p))
27 ++p;
28 if (p == name || p - name > 12 || *p++ != '-')
29 return false;
30 const char *digits_end = p - 1;
31 for (int i = 0; i < 32; ++i)
32 if (!((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f')))
33 return false;
34 else
35 ++p;
36 if (*p++ != '.')
37 return false;
38 if (!mcp_output_mime(p))
39 return false;
40 char number[16];
41 size_t n = (size_t)(digits_end - name);
42 memcpy(number, name, n);
43 number[n] = '\0';
44 int64_t value = (int64_t)strtoll(number, NULL, 10);
45 if (value <= 0)
46 return false;
47 if (expires)
48 *expires = value;
49 return true;
50}
51
52void mcp_download_prune(const char *directory, int64_t now) {
53 uv_fs_t scan;
54 if (uv_fs_scandir(NULL, &scan, directory, 0, NULL) < 0) {
55 uv_fs_req_cleanup(&scan);
56 return;
57 }
58 uv_dirent_t entry;
59 while (uv_fs_scandir_next(&scan, &entry) == 0) {
60 int64_t expires;
61 if (!mcp_download_name(entry.name, &expires) || expires > now)
62 continue;
63 char path[PLATFORM_MAX_PATH_LENGTH];
64 int n = safe_snprintf(path, sizeof(path), "%s/%s", directory, entry.name);
65 if (n < 0 || (size_t)n >= sizeof(path))
66 continue;
67 uv_fs_t op;
68 uv_fs_unlink(NULL, &op, path, NULL);
69 uv_fs_req_cleanup(&op);
70 }
71 uv_fs_req_cleanup(&scan);
72}
73
74asciichat_error_t mcp_download_finalize(const mcp_policy_t *policy, yyjson_mut_doc *doc, int64_t now) {
75 yyjson_mut_val *result = yyjson_mut_obj_get(yyjson_mut_doc_get_root(doc), "result");
76 yyjson_mut_val *data = yyjson_mut_obj_get(result, "structuredContent");
77 yyjson_mut_val *files = yyjson_mut_obj_get(data, "saved_files");
78 yyjson_mut_val *first = yyjson_mut_arr_get_first(files);
79 const char *old = yyjson_mut_get_str(first);
80 if (!old || !*old)
81 return ASCIICHAT_OK;
82 const char *name = strrchr(old, '/');
83 const char *back = strrchr(old, '\\');
84 if (back && (!name || back > name))
85 name = back;
86 name = name ? name + 1 : old;
87 int64_t ignored;
88 if (!mcp_download_name(name, &ignored))
89 return SET_ERRNO(ERROR_INVALID_STATE, "Invalid generated download name");
90 const char *token = strchr(name, '-');
91 int64_t expires = now + 1800;
92 char new_name[128], new_path[PLATFORM_MAX_PATH_LENGTH], url[PLATFORM_MAX_PATH_LENGTH * 2];
93 int n = safe_snprintf(new_name, sizeof(new_name), "%lld%s", (long long)expires, token);
94 if (n < 0 || (size_t)n >= sizeof(new_name))
95 return SET_ERRNO(ERROR_INVALID_STATE, "Download name is too long");
96 n = safe_snprintf(new_path, sizeof(new_path), "%s/%s", policy->downloads_dir, new_name);
97 if (n < 0 || (size_t)n >= sizeof(new_path))
98 return SET_ERRNO(ERROR_INVALID_STATE, "Download path is too long");
99 const char *prefix = policy->downloads_url_prefix;
100 char fallback[128];
101 if (!prefix || !*prefix) {
102 SAFE_SNPRINTF(fallback, sizeof(fallback), "%s://127.0.0.1:%d",
103 policy->tls_cert && *policy->tls_cert ? "https" : "http", policy->port);
104 prefix = fallback;
105 }
106 size_t prefix_len = strlen(prefix);
107 n = safe_snprintf(url, sizeof(url), "%s%sdownloads/%s", prefix,
108 prefix_len && prefix[prefix_len - 1] == '/' ? "" : "/", new_name);
109 if (n < 0 || (size_t)n >= sizeof(url))
110 return SET_ERRNO(ERROR_INVALID_STATE, "Download URL is too long");
111 if (strcmp(old, new_path)) {
112 uv_fs_t fs;
113 int rc = uv_fs_link(NULL, &fs, old, new_path, NULL);
114 uv_fs_req_cleanup(&fs);
115 if (rc < 0)
116 return SET_ERRNO(ERROR_FILE_OPERATION, "Cannot publish download: %s", uv_strerror(rc));
117 uv_fs_unlink(NULL, &fs, old, NULL);
118 uv_fs_req_cleanup(&fs);
119 }
120 time_t t = (time_t)expires;
121 struct tm utc;
122 char timestamp[32];
123 if (platform_gtime(&t, &utc) != ASCIICHAT_OK || !strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &utc))
124 return SET_ERRNO(ERROR_INVALID_STATE, "Cannot format download expiration");
125 yyjson_mut_arr_clear(files);
126 yyjson_mut_arr_add_strcpy(doc, files, url);
127 yyjson_mut_obj_add_strcpy(doc, data, "download_url", url);
128 yyjson_mut_obj_add_strcpy(doc, data, "expires_at", timestamp);
129 yyjson_mut_val *content = yyjson_mut_obj_get(result, "content");
130 yyjson_mut_val *item = yyjson_mut_arr_get_first(content);
131 char description[sizeof(url) + 80];
132 SAFE_SNPRINTF(description, sizeof(description), "Download: %s\nExpires: %s", url, timestamp);
133 yyjson_mut_obj_remove_str(item, "text");
134 yyjson_mut_obj_add_strcpy(doc, item, "text", description);
135 return ASCIICHAT_OK;
136}
#define SET_ERRNO(code,...)
const char *const mcp_output_formats[]
Definition downloads.c:7
void mcp_download_prune(const char *directory, int64_t now)
Definition downloads.c:52
asciichat_error_t mcp_download_finalize(const mcp_policy_t *policy, yyjson_mut_doc *doc, int64_t now)
Definition downloads.c:74
bool mcp_download_name(const char *name, int64_t *expires)
Definition downloads.c:22
const char * mcp_output_mime(const char *extension)
Definition downloads.c:10
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:507
#define PLATFORM_MAX_PATH_LENGTH
Definition common.h:95
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_INVALID_STATE
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_FILE_OPERATION
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
asciichat_error_t platform_gtime(const time_t *timer, struct tm *result)
Platform-safe gmtime wrapper.
const char * tls_cert
Definition mcp.h:40
char downloads_dir[PLATFORM_MAX_PATH_LENGTH]
Definition mcp.h:44
const char * downloads_url_prefix
Definition mcp.h:45
int port
Definition mcp.h:43
⏱️ High-precision timing utilities using sokol_time.h and uthash