tildefriends/src/file.js.c

611 lines
17 KiB
C

#include "file.js.h"
#include "log.h"
#include "mem.h"
#include "task.h"
#include "trace.h"
#include "util.js.h"
#include "unzip.h"
#include "uv.h"
#include <stdbool.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#endif
static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_read_file_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_stat(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_stat_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv);
static double _time_spec_to_double(const uv_timespec_t* time_spec);
static void _file_on_stat_complete(uv_fs_t* request);
typedef struct file_stat_t
{
void* _task;
JSContext* _context;
promiseid_t _promise;
uv_fs_t _request;
} file_stat_t;
typedef struct fs_req_t
{
uv_fs_t fs;
size_t size;
uv_file file;
char buffer[];
} fs_req_t;
void tf_file_register(JSContext* context)
{
JSValue global = JS_GetGlobalObject(context);
JSValue file = JS_NewObject(context);
void* task = JS_GetContextOpaque(context);
const char* zip = tf_task_get_zip_path(task);
JS_SetPropertyStr(context, global, "File", file);
JS_SetPropertyStr(context, file, "readFile", JS_NewCFunction(context, zip ? _file_read_file_zip : _file_read_file, "readFile", 1));
JS_SetPropertyStr(context, file, "writeFile", JS_NewCFunction(context, _file_write_file, "writeFile", 2));
JS_SetPropertyStr(context, file, "stat", JS_NewCFunction(context, zip ? _file_stat_zip : _file_stat, "stat", 1));
JS_FreeValue(context, global);
}
enum
{
k_file_read_max = 8 * 1024 * 1024
};
static void _file_async_close_callback(uv_fs_t* req)
{
uv_fs_req_cleanup(req);
tf_free(req);
}
static void _file_write_write_callback(uv_fs_t* req)
{
uv_fs_req_cleanup(req);
fs_req_t* fsreq = (fs_req_t*)req;
tf_task_t* task = req->loop->data;
JSContext* context = tf_task_get_context(task);
promiseid_t promise = (promiseid_t)(intptr_t)req->data;
if (req->result >= 0)
{
tf_task_resolve_promise(task, promise, JS_NewInt64(context, req->result));
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to write %s: %s", req->path, uv_strerror(req->result)));
}
int result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_free(fsreq);
}
}
static void _file_write_open_callback(uv_fs_t* req)
{
fs_req_t* fsreq = (fs_req_t*)req;
const char* path = tf_strdup(req->path);
uv_fs_req_cleanup(req);
tf_task_t* task = req->loop->data;
JSContext* context = tf_task_get_context(task);
promiseid_t promise = (promiseid_t)(intptr_t)req->data;
if (req->result >= 0)
{
uv_buf_t buf = { .base = fsreq->buffer, .len = fsreq->size };
fsreq->file = req->result;
int result = uv_fs_write(req->loop, req, fsreq->file, &buf, 1, 0, _file_write_write_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to write %s: %s", path, uv_strerror(result)));
result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_free(fsreq);
}
}
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "%s", uv_strerror(req->result)));
tf_free(req);
}
tf_free((void*)path);
}
static JSValue _file_write_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
const char* file_name = JS_ToCString(context, argv[0]);
size_t size;
uint8_t* buffer = tf_util_try_get_array_buffer(context, &size, argv[1]);
bool is_array_buffer = false;
if (buffer)
{
is_array_buffer = true;
}
else
{
buffer = (uint8_t*)JS_ToCStringLen(context, &size, argv[1]);
}
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
fs_req_t* req = tf_malloc(sizeof(fs_req_t) + size);
*req = (fs_req_t)
{
.fs =
{
.data = (void*)(intptr_t)promise,
},
.size = size,
};
memcpy(req->buffer, buffer, size);
if (!is_array_buffer)
{
JS_FreeCString(context, (const char*)buffer);
}
int result = uv_fs_open(tf_task_get_loop(task), &req->fs, file_name, UV_FS_O_CREAT | UV_FS_O_WRONLY, 0644, _file_write_open_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for write: %s", file_name, uv_strerror(result)));
}
JS_FreeCString(context, file_name);
return promise_value;
}
static void _file_read_read_callback(uv_fs_t* req)
{
fs_req_t* fsreq = (fs_req_t*)req;
tf_task_t* task = req->loop->data;
JSContext* context = tf_task_get_context(task);
promiseid_t promise = (promiseid_t)(intptr_t)req->data;
if (req->result >= 0)
{
JSValue array = tf_util_new_uint8_array(context, (const uint8_t*)fsreq->buffer, req->result);
tf_task_resolve_promise(task, promise, array);
JS_FreeValue(context, array);
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to read %s: %s", req->path, uv_strerror(req->result)));
}
uv_fs_req_cleanup(req);
int result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_free(fsreq);
}
}
static void _file_read_open_callback(uv_fs_t* req)
{
const char* path = tf_strdup(req->path);
uv_fs_req_cleanup(req);
fs_req_t* fsreq = (fs_req_t*)req;
tf_task_t* task = req->loop->data;
JSContext* context = tf_task_get_context(task);
promiseid_t promise = (promiseid_t)(intptr_t)req->data;
if (req->result >= 0)
{
uv_buf_t buf = { .base = fsreq->buffer, .len = fsreq->size };
fsreq->file = req->result;
int result = uv_fs_read(req->loop, req, fsreq->file, &buf, 1, 0, _file_read_read_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to read %s: %s", path, uv_strerror(result)));
result = uv_fs_close(req->loop, req, fsreq->file, _file_async_close_callback);
if (result < 0)
{
uv_fs_req_cleanup(req);
tf_free(fsreq);
}
}
}
else
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for read: %s", path, uv_strerror(req->result)));
uv_fs_req_cleanup(req);
tf_free(req);
}
tf_free((void*)path);
}
static JSValue _file_read_file(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
const char* file_name = JS_ToCString(context, argv[0]);
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
fs_req_t* req = tf_malloc(sizeof(fs_req_t) + k_file_read_max);
*req = (fs_req_t)
{
.fs =
{
.data = (void*)(intptr_t)promise,
},
.size = k_file_read_max,
};
memset(req + 1, 0, k_file_read_max);
int result = uv_fs_open(tf_task_get_loop(task), &req->fs, file_name, UV_FS_O_RDONLY, 0, _file_read_open_callback);
if (result < 0)
{
tf_task_reject_promise(task, promise, JS_ThrowInternalError(context, "Failed to open %s for read: %s", file_name, uv_strerror(result)));
uv_fs_req_cleanup(&req->fs);
tf_free(req);
}
JS_FreeCString(context, file_name);
return promise_value;
}
typedef struct _zip_read_work_t
{
uv_work_t request;
JSContext* context;
tf_task_t* task;
const char* file_path;
promiseid_t promise;
int result;
uint8_t* buffer;
size_t size;
} zip_read_work_t;
static void _file_read_file_zip_work(uv_work_t* work)
{
zip_read_work_t* data = work->data;
tf_trace_t* trace = tf_task_get_trace(data->task);
tf_trace_begin(trace, "file_read_zip_work");
unzFile zip = unzOpen(tf_task_get_zip_path(data->task));
bool is_file_open = false;
if (!zip)
{
data->result = errno;
tf_trace_end(trace);
return;
}
data->result = unzLocateFile(zip, data->file_path, 1);
if (data->result != UNZ_OK)
{
goto done;
}
unz_file_info64 info = { 0 };
data->result = unzGetCurrentFileInfo64(zip, &info, NULL, 0, NULL, 0, NULL, 0);
if (data->result != UNZ_OK)
{
goto done;
}
data->result = unzOpenCurrentFile(zip);
if (data->result != UNZ_OK)
{
goto done;
}
is_file_open = true;
data->buffer = tf_malloc(info.uncompressed_size);
data->result = unzReadCurrentFile(zip, data->buffer, info.uncompressed_size);
if (data->result <= 0)
{
tf_free(data->buffer);
data->buffer = NULL;
}
tf_trace_end(trace);
done:
if (is_file_open)
{
unzCloseCurrentFile(zip);
}
unzClose(zip);
}
static void _file_read_file_zip_after_work(uv_work_t* work, int status)
{
zip_read_work_t* data = work->data;
tf_trace_t* trace = tf_task_get_trace(data->task);
tf_trace_begin(trace, "file_read_zip_after_work");
if (data->result >= 0)
{
JSValue array = tf_util_new_uint8_array(data->context, data->buffer, data->result);
tf_task_resolve_promise(data->task, data->promise, array);
JS_FreeValue(data->context, array);
}
else
{
tf_task_reject_promise(data->task, data->promise, JS_ThrowInternalError(data->context, "Failed to read %s: %d.", data->file_path, data->result));
}
tf_free(data->buffer);
tf_free((void*)data->file_path);
tf_free(data);
tf_trace_end(trace);
}
static JSValue _file_read_file_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
tf_task_t* task = JS_GetContextOpaque(context);
const char* file_name = JS_ToCString(context, argv[0]);
zip_read_work_t* work = tf_malloc(sizeof(zip_read_work_t));
*work = (zip_read_work_t)
{
.request =
{
.data = work,
},
.context = context,
.task = task,
.file_path = tf_strdup(file_name),
};
JSValue promise_value = tf_task_allocate_promise(task, &work->promise);
int r = uv_queue_work(tf_task_get_loop(task), &work->request, _file_read_file_zip_work, _file_read_file_zip_after_work);
if (r)
{
tf_task_reject_promise(task, work->promise, JS_ThrowInternalError(context, "Failed to create read work for %s: %s", file_name, uv_strerror(r)));
tf_free((void*)work->file_path);
tf_free(work);
}
JS_FreeCString(context, file_name);
return promise_value;
}
static JSValue _file_stat(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
const char* path = JS_ToCString(context, argv[0]);
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
file_stat_t* data = tf_malloc(sizeof(file_stat_t));
data->_task = task;
data->_promise = promise;
data->_request.data = data;
data->_context = context;
int result = uv_fs_stat(tf_task_get_loop(task), &data->_request, path, _file_on_stat_complete);
if (result)
{
tf_task_reject_promise(task, promise, JS_NewInt32(context, result));
uv_fs_req_cleanup(&data->_request);
tf_free(data);
}
JS_FreeCString(context, path);
return promise_value;
}
static JSValue _file_stat_zip(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv)
{
void* task = JS_GetContextOpaque(context);
promiseid_t promise = -1;
JSValue promise_value = tf_task_allocate_promise(task, &promise);
file_stat_t* data = tf_malloc(sizeof(file_stat_t));
data->_task = task;
data->_promise = promise;
data->_request.data = data;
data->_context = context;
/* Ignore the requested path and stat the zip itself. */
int result = uv_fs_stat(tf_task_get_loop(task), &data->_request, tf_task_get_zip_path(task), _file_on_stat_complete);
if (result)
{
tf_task_reject_promise(task, promise, JS_NewInt32(context, result));
uv_fs_req_cleanup(&data->_request);
tf_free(data);
}
return promise_value;
}
static double _time_spec_to_double(const uv_timespec_t* time_spec)
{
return time_spec->tv_sec + (double)(time_spec->tv_nsec) / 1e9;
}
static void _file_on_stat_complete(uv_fs_t* request)
{
file_stat_t* data = (file_stat_t*)(request->data);
JSContext* context = data->_context;
if (request->result)
{
tf_task_reject_promise(data->_task, data->_promise, JS_NewInt32(context, request->result));
}
else
{
JSValue result = JS_NewObject(context);
JS_SetPropertyStr(context, result, "mtime", JS_NewFloat64(context, _time_spec_to_double(&request->statbuf.st_mtim)));
JS_SetPropertyStr(context, result, "ctime", JS_NewFloat64(context, _time_spec_to_double(&request->statbuf.st_ctim)));
JS_SetPropertyStr(context, result, "atime", JS_NewFloat64(context, _time_spec_to_double(&request->statbuf.st_atim)));
JS_SetPropertyStr(context, result, "size", JS_NewFloat64(context, request->statbuf.st_size));
tf_task_resolve_promise(data->_task, data->_promise, result);
JS_FreeValue(context, result);
}
uv_fs_req_cleanup(request);
tf_free(data);
}
typedef struct _stat_t
{
uv_fs_t request;
tf_task_t* task;
void (*callback)(tf_task_t* task, const char* path, int result, const uv_stat_t* stat, void* user_data);
void* user_data;
char path[];
} stat_t;
static void _file_stat_complete(uv_fs_t* request)
{
stat_t* data = request->data;
data->callback(data->task, data->path, request->result, &request->statbuf, data->user_data);
uv_fs_req_cleanup(request);
tf_free(data);
}
void tf_file_stat(tf_task_t* task, const char* path, void (*callback)(tf_task_t* task, const char* path, int result, const uv_stat_t* stat, void* user_data), void* user_data)
{
if (!path)
{
callback(task, path, -EINVAL, NULL, user_data);
return;
}
const char* zip = tf_task_get_zip_path(task);
size_t path_length = strlen(path) + 1;
stat_t* data = tf_malloc(sizeof(stat_t) + path_length);
*data = (stat_t) { .request = { .data = data }, .task = task, .callback = callback, .user_data = user_data };
memcpy(data->path, path, path_length);
int result = uv_fs_stat(tf_task_get_loop(task), &data->request, zip ? zip : path, _file_stat_complete);
if (result)
{
callback(task, path, result, NULL, user_data);
uv_fs_req_cleanup(&data->request);
tf_free(data);
}
}
typedef struct _read_t
{
uv_work_t work;
tf_task_t* task;
void (*callback)(tf_task_t* task, const char* path, int result, const void* data, void* user_data);
void* user_data;
int64_t result;
char buffer[k_file_read_max];
char path[];
} read_t;
static void _file_read_work(uv_work_t* work)
{
read_t* data = work->data;
const char* zip_path = tf_task_get_zip_path(data->task);
if (zip_path)
{
tf_trace_t* trace = tf_task_get_trace(data->task);
tf_trace_begin(trace, "file_read_zip_work");
unzFile zip = unzOpen(zip_path);
if (zip)
{
data->result = unzLocateFile(zip, data->path, 1);
if (data->result == UNZ_OK)
{
unz_file_info64 info = { 0 };
data->result = unzGetCurrentFileInfo64(zip, &info, NULL, 0, NULL, 0, NULL, 0);
if (data->result == UNZ_OK && info.uncompressed_size > sizeof(data->buffer))
{
data->result = -EFBIG;
}
else if (data->result == UNZ_OK)
{
data->result = unzOpenCurrentFile(zip);
if (data->result == UNZ_OK)
{
data->result = unzReadCurrentFile(zip, data->buffer, info.uncompressed_size);
if (data->result == (int64_t)info.uncompressed_size)
{
int r = unzCloseCurrentFile(zip);
if (r != UNZ_OK)
{
data->result = r;
}
}
else
{
data->result = EAGAIN;
}
}
}
}
unzClose(zip);
}
else
{
data->result = errno;
}
tf_trace_end(trace);
}
else
{
tf_trace_t* trace = tf_task_get_trace(data->task);
tf_trace_begin(trace, "file_read_zip_work");
uv_loop_t* loop = tf_task_get_loop(data->task);
uv_fs_t open_req = { 0 };
int open_result = uv_fs_open(loop, &open_req, data->path, UV_FS_O_RDONLY, 0, NULL);
if (open_result >= 0)
{
uv_buf_t buf = { .base = data->buffer, .len = sizeof(data->buffer) };
uv_fs_t read_req = { 0 };
int result = uv_fs_read(loop, &read_req, open_result, &buf, 1, 0, NULL);
if ((size_t)result >= sizeof(data->buffer))
{
data->result = -EFBIG;
}
else
{
data->result = result;
}
uv_fs_req_cleanup(&read_req);
uv_fs_t close_req = { 0 };
result = uv_fs_close(loop, &close_req, open_result, NULL);
if (result && data->result >= 0)
{
data->result = result;
}
uv_fs_req_cleanup(&close_req);
}
else
{
data->result = errno;
}
uv_fs_req_cleanup(&open_req);
tf_trace_end(trace);
}
}
static void _file_read_after_work(uv_work_t* work, int result)
{
read_t* data = work->data;
data->callback(data->task, data->path, data->result, data->buffer, data->user_data);
tf_free(data);
}
void tf_file_read(tf_task_t* task, const char* path, void (*callback)(tf_task_t* task, const char* path, int result, const void* data, void* user_data), void* user_data)
{
if (!path)
{
callback(task, path, -EINVAL, NULL, user_data);
return;
}
size_t path_length = strlen(path) + 1;
read_t* data = tf_malloc(sizeof(read_t) + path_length);
memset(data, 0, sizeof(read_t));
data->callback = callback;
data->user_data = user_data;
data->work.data = data;
data->task = task;
memcpy(data->path, path, path_length);
int r = uv_queue_work(tf_task_get_loop(task), &data->work, _file_read_work, _file_read_after_work);
if (r)
{
_file_read_after_work(&data->work, r);
}
}