2 assfile - library for accessing assets with an fopen/fread-like interface
3 Copyright (C) 2018 John Tsiombikas <nuclear@member.fsf.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU Lesser General Public License for more details.
15 You should have received a copy of the GNU Lesser General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
22 #include "assfile_impl.h"
26 #include <curl/curl.h>
44 /* fopen-thread waits until the state becomes known (request starts transmitting or fails) */
46 pthread_cond_t state_cond;
47 pthread_mutex_t state_mutex;
50 static void *fop_open(const char *fname, void *udata);
51 static void fop_close(void *fp, void *udata);
52 static long fop_seek(void *fp, long offs, int whence, void *udata);
53 static long fop_read(void *fp, void *buf, long size, void *udata);
55 static void exit_cleanup(void);
56 static void download(void *data);
57 static size_t recv_callback(char *ptr, size_t size, size_t nmemb, void *udata);
58 static const char *get_temp_dir(void);
59 static int mkdir_path(const char *path);
61 static char *tmpdir, *cachedir;
62 static struct thread_pool *tpool;
65 struct ass_fileops *ass_alloc_url(const char *url)
68 struct ass_fileops *fop;
73 curl_global_init(CURL_GLOBAL_ALL);
76 if(!*ass_mod_url_cachedir) {
77 strcpy(ass_mod_url_cachedir, "assfile_cache");
79 tmpdir = (char*)get_temp_dir();
80 if(!(cachedir = malloc(strlen(ass_mod_url_cachedir) + strlen(tmpdir) + 2))) {
81 fprintf(stderr, "assfile mod_url: failed to allocate cachedir path buffer\n");
84 sprintf(cachedir, "%s/%s", tmpdir, ass_mod_url_cachedir);
86 if(mkdir_path(cachedir) == -1) {
87 fprintf(stderr, "assfile mod_url: failed to create cache directory: %s\n", cachedir);
91 if(ass_mod_url_max_threads <= 0) {
92 ass_mod_url_max_threads = 8;
95 if(!(curl = calloc(ass_mod_url_max_threads, sizeof *curl))) {
96 perror("assfile: failed to allocate curl context table");
99 for(i=0; i<ass_mod_url_max_threads; i++) {
100 if(!(curl[i] = curl_easy_init())) {
103 curl_easy_setopt(curl[i], CURLOPT_WRITEFUNCTION, recv_callback);
106 if(!(tpool = ass_tpool_create(ass_mod_url_max_threads))) {
107 fprintf(stderr, "assfile: failed to create thread pool\n");
114 if(!(fop = malloc(sizeof *fop))) {
118 if(!(fop->udata = malloc(len + 1))) {
122 memcpy(fop->udata, url, len + 1);
124 ptr = (char*)fop->udata + len - 1;
125 while(*ptr == '/') *ptr-- = 0;
128 fop->open = fop_open;
129 fop->close = fop_close;
130 fop->seek = fop_seek;
131 fop->read = fop_read;
137 for(i=0; i<ass_mod_url_max_threads; i++) {
139 curl_easy_cleanup(curl[i]);
147 static void exit_cleanup(void)
152 ass_tpool_destroy(tpool);
155 for(i=0; i<ass_mod_url_max_threads; i++) {
157 curl_easy_cleanup(curl[i]);
162 curl_global_cleanup();
166 void ass_free_url(struct ass_fileops *fop)
171 static char *cache_filename(const char *fname, const char *url)
174 unsigned char sum[16];
178 int url_len = strlen(url);
181 MD4Update(&md4ctx, (unsigned char*)url, url_len);
182 MD4Final((unsigned char*)sum, &md4ctx);
184 for(i=0; i<16; i++) {
185 sprintf(sumstr + i * 2, "%02x", (unsigned int)sum[i]);
189 prefix_len = strlen(cachedir);
190 if(!(resfname = malloc(prefix_len + 64))) {
193 sprintf(resfname, "%s/%s", cachedir, sumstr);
197 static void *fop_open(const char *fname, void *udata)
199 struct file_info *file;
201 char *prefix = udata;
203 if(!fname || !*fname) {
208 if(!(file = malloc(sizeof *file))) {
213 if(!(file->url = malloc(strlen(prefix) + strlen(fname) + 2))) {
214 perror("assfile: mod_url: failed to allocate url buffer");
219 if(prefix && *prefix) {
220 sprintf(file->url, "%s/%s", prefix, fname);
222 strcpy(file->url, fname);
225 if(!(file->cache_fname = cache_filename(fname, file->url))) {
231 if(!(file->cache_file = fopen(file->cache_fname, "wb"))) {
232 fprintf(stderr, "assfile: mod_url: failed to open cache file (%s) for writing: %s\n",
233 file->cache_fname, strerror(errno));
236 free(file->cache_fname);
241 file->state = DL_UNKNOWN;
242 pthread_mutex_init(&file->state_mutex, 0);
243 pthread_cond_init(&file->state_cond, 0);
246 fprintf(stderr, "assfile: mod_url: get \"%s\" -> \"%s\"\n", file->url, file->cache_fname);
248 ass_tpool_enqueue(tpool, file, download, 0);
250 /* wait until the file changes state */
251 pthread_mutex_lock(&file->state_mutex);
252 while(file->state == DL_UNKNOWN) {
253 pthread_cond_wait(&file->state_cond, &file->state_mutex);
256 pthread_mutex_unlock(&file->state_mutex);
258 if(state == DL_ERROR) {
259 /* the worker stopped, so we can safely cleanup and return error */
260 fclose(file->cache_file);
261 remove(file->cache_fname);
262 free(file->cache_fname);
264 pthread_cond_destroy(&file->state_cond);
265 pthread_mutex_destroy(&file->state_mutex);
267 ass_errno = ENOENT; /* TODO: differentiate between 403 and 404 */
273 static void wait_done(struct file_info *file)
275 pthread_mutex_lock(&file->state_mutex);
276 while(file->state != DL_DONE && file->state != DL_ERROR) {
277 pthread_cond_wait(&file->state_cond, &file->state_mutex);
279 pthread_mutex_unlock(&file->state_mutex);
282 static void fop_close(void *fp, void *udata)
284 struct file_info *file = fp;
286 wait_done(file); /* TODO: stop download instead of waiting to finish */
288 fclose(file->cache_file);
289 if(file->state == DL_ERROR) remove(file->cache_fname);
290 free(file->cache_fname);
292 pthread_cond_destroy(&file->state_cond);
293 pthread_mutex_destroy(&file->state_mutex);
297 static long fop_seek(void *fp, long offs, int whence, void *udata)
299 struct file_info *file = fp;
302 fseek(file->cache_file, offs, whence);
303 return ftell(file->cache_file);
306 static long fop_read(void *fp, void *buf, long size, void *udata)
308 struct file_info *file = fp;
311 return fread(buf, 1, size, file->cache_file);
314 /* this is the function called by the worker threads to perform the download
315 * signal state changes, and prepare the cache file for reading
317 static void download(void *data)
320 struct file_info *file = data;
322 tid = ass_tpool_thread_id(tpool);
324 curl_easy_setopt(curl[tid], CURLOPT_URL, file->url);
325 curl_easy_setopt(curl[tid], CURLOPT_WRITEDATA, file);
326 res = curl_easy_perform(curl[tid]);
328 pthread_mutex_lock(&file->state_mutex);
329 if(res == CURLE_OK) {
330 file->state = DL_DONE;
331 fclose(file->cache_file);
332 if(!(file->cache_file = fopen(file->cache_fname, "rb"))) {
333 fprintf(stderr, "assfile: failed to reopen cache file (%s) for reading: %s\n",
334 file->cache_fname, strerror(errno));
335 file->state = DL_ERROR;
338 file->state = DL_ERROR;
340 pthread_cond_broadcast(&file->state_cond);
341 pthread_mutex_unlock(&file->state_mutex);
344 /* this function is called by curl to pass along downloaded data chunks */
345 static size_t recv_callback(char *ptr, size_t size, size_t count, void *udata)
347 struct file_info *file = udata;
349 pthread_mutex_lock(&file->state_mutex);
350 if(file->state == DL_UNKNOWN) {
351 file->state = DL_STARTED;
352 pthread_cond_broadcast(&file->state_cond);
354 pthread_mutex_unlock(&file->state_mutex);
356 return fwrite(ptr, size, count, file->cache_file);
362 static const char *get_temp_dir(void)
364 static char buf[MAX_PATH + 1];
365 GetTempPathA(MAX_PATH + 1, buf);
369 static const char *get_temp_dir(void)
371 char *env = getenv("TMPDIR");
372 return env ? env : "/tmp";
377 static int mkdir_path(const char *path)
379 char *pathbuf, *dptr;
382 if(!path || !*path) return -1;
384 pathbuf = dptr = alloca(strlen(path) + 1);
391 if(c == '/' || c == '\\') break;
395 if(stat(pathbuf, &st) == -1) {
396 /* path component does not exist, create it */
398 if(mkdir(pathbuf) == -1) {
400 if(mkdir(pathbuf, 0777) == -1) {
410 #else /* don't build mod_url */
411 struct ass_fileops *ass_alloc_url(const char *url)
413 fprintf(stderr, "assfile: compiled without URL asset source support\n");
417 void ass_free_url(struct ass_fileops *fop)