477 lines
14 KiB
C
477 lines
14 KiB
C
#include "qop.h"
|
|
#include "cell.h"
|
|
|
|
static JSClassID js_qop_archive_class_id;
|
|
|
|
static void js_qop_archive_finalizer(JSRuntime *rt, JSValue val) {
|
|
qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id);
|
|
if (qop) {
|
|
if (qop->hashmap) {
|
|
js_free_rt(rt, qop->hashmap);
|
|
}
|
|
qop_close(qop);
|
|
js_free_rt(rt, qop);
|
|
}
|
|
}
|
|
|
|
static JSClassDef js_qop_archive_class = {
|
|
"qop archive",
|
|
.finalizer = js_qop_archive_finalizer,
|
|
};
|
|
|
|
static JSClassID js_qop_writer_class_id;
|
|
|
|
typedef struct {
|
|
FILE *fh;
|
|
qop_file *files;
|
|
int len;
|
|
int capacity;
|
|
unsigned int size;
|
|
} qop_writer;
|
|
|
|
static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
|
|
qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
|
|
if (w) {
|
|
if (w->fh) fclose(w->fh);
|
|
if (w->files) js_free_rt(rt, w->files);
|
|
js_free_rt(rt, w);
|
|
}
|
|
}
|
|
|
|
static JSClassDef js_qop_writer_class = {
|
|
"qop writer",
|
|
.finalizer = js_qop_writer_finalizer,
|
|
};
|
|
|
|
static qop_writer *js2writer(JSContext *js, JSValue v) {
|
|
return JS_GetOpaque(v, js_qop_writer_class_id);
|
|
}
|
|
|
|
// Helper functions for writer
|
|
static void write_16(unsigned int v, FILE *fh) {
|
|
unsigned char b[2];
|
|
b[0] = 0xff & (v);
|
|
b[1] = 0xff & (v >> 8);
|
|
fwrite(b, 2, 1, fh);
|
|
}
|
|
|
|
static void write_32(unsigned int v, FILE *fh) {
|
|
unsigned char b[4];
|
|
b[0] = 0xff & (v);
|
|
b[1] = 0xff & (v >> 8);
|
|
b[2] = 0xff & (v >> 16);
|
|
b[3] = 0xff & (v >> 24);
|
|
fwrite(b, 4, 1, fh);
|
|
}
|
|
|
|
static void write_64(unsigned long long v, FILE *fh) {
|
|
unsigned char b[8];
|
|
b[0] = 0xff & (v);
|
|
b[1] = 0xff & (v >> 8);
|
|
b[2] = 0xff & (v >> 16);
|
|
b[3] = 0xff & (v >> 24);
|
|
b[4] = 0xff & (v >> 32);
|
|
b[5] = 0xff & (v >> 40);
|
|
b[6] = 0xff & (v >> 48);
|
|
b[7] = 0xff & (v >> 56);
|
|
fwrite(b, 8, 1, fh);
|
|
}
|
|
|
|
static unsigned long long qop_hash(const char *key) {
|
|
unsigned long long h = 525201411107845655ull;
|
|
for (;*key;++key) {
|
|
h ^= (unsigned char)*key;
|
|
h *= 0x5bd1e9955bd1e995ull;
|
|
h ^= h >> 47;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static qop_desc *js2qop(JSContext *js, JSValue v) {
|
|
return JS_GetOpaque(v, js_qop_archive_class_id);
|
|
}
|
|
|
|
static int js_qop_ensure_index(JSContext *js, qop_desc *qop) {
|
|
if (qop->hashmap != NULL) return 1;
|
|
void *buffer = js_malloc(js, qop->hashmap_size);
|
|
if (!buffer) return 0;
|
|
int num = qop_read_index(qop, buffer);
|
|
if (num == 0) {
|
|
js_free(js, buffer);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
JSC_CCALL(qop_open,
|
|
size_t len;
|
|
void *data = js_get_blob_data(js, &len, argv[0]);
|
|
if (!data)
|
|
ret = JS_ThrowReferenceError(js, "Could not get blob data.\n");
|
|
else {
|
|
qop_desc *qop = js_malloc(js, sizeof(qop_desc));
|
|
if (!qop)
|
|
ret = JS_ThrowOutOfMemory(js);
|
|
else {
|
|
int size = qop_open_data((const unsigned char *)data, len, qop);
|
|
if (size == 0) {
|
|
js_free(js, qop);
|
|
ret = JS_ThrowReferenceError(js, "Failed to open QOP archive from blob");
|
|
} else {
|
|
JSValue obj = JS_NewObjectClass(js, js_qop_archive_class_id);
|
|
JS_SetOpaque(obj, qop);
|
|
ret = obj;
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
JSC_CCALL(qop_write,
|
|
const char *path = JS_ToCString(js, argv[0]);
|
|
if (!path) return JS_EXCEPTION;
|
|
|
|
FILE *fh = fopen(path, "wb");
|
|
JS_FreeCString(js, path);
|
|
if (!fh) return JS_ThrowInternalError(js, "Could not open file for writing");
|
|
|
|
qop_writer *w = js_malloc(js, sizeof(qop_writer));
|
|
if (!w) {
|
|
fclose(fh);
|
|
return JS_ThrowOutOfMemory(js);
|
|
}
|
|
|
|
w->fh = fh;
|
|
w->capacity = 1024;
|
|
w->len = 0;
|
|
w->size = 0;
|
|
w->files = js_malloc(js, sizeof(qop_file) * w->capacity);
|
|
if (!w->files) {
|
|
fclose(fh);
|
|
js_free(js, w);
|
|
return JS_ThrowOutOfMemory(js);
|
|
}
|
|
|
|
JSValue obj = JS_NewObjectClass(js, js_qop_writer_class_id);
|
|
JS_SetOpaque(obj, w);
|
|
ret = obj;
|
|
)
|
|
|
|
|
|
static JSValue js_qop_close(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_desc *qop = js2qop(js, self);
|
|
if (!qop)
|
|
return JS_ThrowInternalError(js, "Invalid QOP archive");
|
|
|
|
qop_close(qop);
|
|
JS_SetOpaque(self, NULL); // Prevent double free
|
|
return JS_NULL;
|
|
}
|
|
|
|
|
|
|
|
static JSValue js_qop_read(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_desc *qop = js2qop(js, self);
|
|
if (!qop)
|
|
return JS_ThrowInternalError(js, "Invalid QOP archive");
|
|
|
|
const char *path = JS_ToCString(js, argv[0]);
|
|
if (!path)
|
|
return JS_EXCEPTION;
|
|
|
|
if (!js_qop_ensure_index(js, qop)) {
|
|
JS_FreeCString(js, path);
|
|
return JS_ThrowReferenceError(js, "Failed to read QOP index");
|
|
}
|
|
|
|
qop_file *file = qop_find(qop, path);
|
|
JS_FreeCString(js, path);
|
|
|
|
if (!file)
|
|
return JS_NULL;
|
|
|
|
unsigned char *dest = js_malloc(js, file->size);
|
|
if (!dest)
|
|
return JS_ThrowOutOfMemory(js);
|
|
|
|
int bytes = qop_read(qop, file, dest);
|
|
if (bytes == 0) {
|
|
js_free(js, dest);
|
|
return JS_ThrowReferenceError(js, "Failed to read file");
|
|
}
|
|
|
|
JSValue blob = js_new_blob_stoned_copy(js, dest, bytes);
|
|
js_free(js, dest);
|
|
return blob;
|
|
}
|
|
|
|
static JSValue js_qop_read_ex(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_desc *qop = js2qop(js, self);
|
|
if (!qop)
|
|
return JS_ThrowInternalError(js, "Invalid QOP archive");
|
|
|
|
const char *path = JS_ToCString(js, argv[0]);
|
|
if (!path)
|
|
return JS_EXCEPTION;
|
|
|
|
if (!js_qop_ensure_index(js, qop)) {
|
|
JS_FreeCString(js, path);
|
|
return JS_ThrowReferenceError(js, "Failed to read QOP index");
|
|
}
|
|
|
|
qop_file *file = qop_find(qop, path);
|
|
JS_FreeCString(js, path);
|
|
|
|
if (!file)
|
|
return JS_NULL;
|
|
|
|
unsigned int start;
|
|
unsigned int len;
|
|
if (JS_ToUint32(js, &start, argv[1]) < 0 || JS_ToUint32(js, &len, argv[2]) < 0)
|
|
return JS_ThrowTypeError(js, "Invalid start or len");
|
|
|
|
unsigned char *dest = js_malloc(js, len);
|
|
if (!dest)
|
|
return JS_ThrowOutOfMemory(js);
|
|
|
|
int bytes = qop_read_ex(qop, file, dest, start, len);
|
|
if (bytes == 0) {
|
|
js_free(js, dest);
|
|
return JS_ThrowReferenceError(js, "Failed to read file part");
|
|
}
|
|
|
|
JSValue blob = js_new_blob_stoned_copy(js, dest, bytes);
|
|
js_free(js, dest);
|
|
return blob;
|
|
}
|
|
|
|
static JSValue js_qop_list(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_desc *qop = js2qop(js, self);
|
|
if (!qop)
|
|
return JS_ThrowInternalError(js, "Invalid QOP archive");
|
|
|
|
if (!js_qop_ensure_index(js, qop)) {
|
|
return JS_ThrowReferenceError(js, "Failed to read QOP index");
|
|
}
|
|
|
|
JSValue arr = JS_NewArray(js);
|
|
int count = 0;
|
|
|
|
for (unsigned int i = 0; i < qop->hashmap_len; i++) {
|
|
qop_file *file = &qop->hashmap[i];
|
|
if (file->size == 0) continue; // empty slot
|
|
|
|
char *path = js_malloc(js, file->path_len);
|
|
if (!path) {
|
|
return JS_ThrowOutOfMemory(js);
|
|
}
|
|
|
|
int len = qop_read_path(qop, file, path);
|
|
if (len == 0) {
|
|
js_free(js, path);
|
|
continue; // skip on error
|
|
}
|
|
|
|
JSValue str = JS_NewStringLen(js, path, len - 1); // -1 for null terminator
|
|
js_free(js, path);
|
|
JS_SetPropertyUint32(js, arr, count++, str);
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
static JSValue js_qop_stat(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_desc *qop = js2qop(js, self);
|
|
if (!qop) return JS_ThrowInternalError(js, "Invalid QOP archive");
|
|
|
|
const char *path = JS_ToCString(js, argv[0]);
|
|
if (!path) return JS_EXCEPTION;
|
|
|
|
if (!js_qop_ensure_index(js, qop)) {
|
|
JS_FreeCString(js, path);
|
|
return JS_ThrowReferenceError(js, "Failed to read QOP index");
|
|
}
|
|
|
|
qop_file *file = qop_find(qop, path);
|
|
JS_FreeCString(js, path);
|
|
|
|
if (!file) return JS_NULL;
|
|
|
|
JSValue obj = JS_NewObject(js);
|
|
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, file->size));
|
|
JS_SetPropertyStr(js, obj, "modtime", JS_NewInt64(js, 0));
|
|
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, 0));
|
|
return obj;
|
|
}
|
|
|
|
static JSValue js_qop_is_directory(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_desc *qop = js2qop(js, self);
|
|
if (!qop) return JS_ThrowInternalError(js, "Invalid QOP archive");
|
|
|
|
const char *path = JS_ToCString(js, argv[0]);
|
|
if (!path) return JS_EXCEPTION;
|
|
|
|
if (!js_qop_ensure_index(js, qop)) {
|
|
JS_FreeCString(js, path);
|
|
return JS_ThrowReferenceError(js, "Failed to read QOP index");
|
|
}
|
|
|
|
// Check if any file starts with path + "/"
|
|
size_t path_len = strlen(path);
|
|
char *prefix = js_malloc(js, path_len + 2);
|
|
memcpy(prefix, path, path_len);
|
|
prefix[path_len] = '/';
|
|
prefix[path_len + 1] = '\0';
|
|
|
|
int found = 0;
|
|
|
|
// This is inefficient but simple. QOP doesn't have a directory structure.
|
|
// We iterate all files to check prefixes.
|
|
for (unsigned int i = 0; i < qop->hashmap_len; i++) {
|
|
qop_file *file = &qop->hashmap[i];
|
|
if (file->size == 0) continue;
|
|
|
|
// We need to read the path to check it
|
|
// Optimization: check if we can read just the prefix?
|
|
// qop_read_path reads the whole path.
|
|
|
|
// Let's read the path.
|
|
char file_path[1024]; // MAX_PATH_LEN
|
|
if (file->path_len > 1024) continue; // Should not happen based on spec
|
|
|
|
qop_read_path(qop, file, file_path);
|
|
if (strncmp(file_path, prefix, path_len + 1) == 0) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
js_free(js, prefix);
|
|
JS_FreeCString(js, path);
|
|
return JS_NewBool(js, found);
|
|
}
|
|
|
|
// Writer methods
|
|
|
|
static JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_writer *w = js2writer(js, self);
|
|
if (!w) return JS_ThrowInternalError(js, "Invalid QOP writer");
|
|
|
|
const char *path = JS_ToCString(js, argv[0]);
|
|
if (!path) return JS_EXCEPTION;
|
|
|
|
size_t data_len;
|
|
void *data = js_get_blob_data(js, &data_len, argv[1]);
|
|
if (!data) {
|
|
JS_FreeCString(js, path);
|
|
return JS_ThrowTypeError(js, "Second argument must be a blob");
|
|
}
|
|
|
|
if (w->len >= w->capacity) {
|
|
w->capacity *= 2;
|
|
qop_file *new_files = js_realloc(js, w->files, sizeof(qop_file) * w->capacity);
|
|
if (!new_files) {
|
|
JS_FreeCString(js, path);
|
|
return JS_ThrowOutOfMemory(js);
|
|
}
|
|
w->files = new_files;
|
|
}
|
|
|
|
// Strip leading "./"
|
|
const char *archive_path = path;
|
|
if (path[0] == '.' && path[1] == '/') {
|
|
archive_path = path + 2;
|
|
}
|
|
|
|
unsigned long long hash = qop_hash(archive_path);
|
|
int path_len = strlen(archive_path) + 1;
|
|
|
|
// Write path
|
|
fwrite(archive_path, 1, path_len, w->fh);
|
|
|
|
// Write data
|
|
fwrite(data, 1, data_len, w->fh);
|
|
|
|
w->files[w->len] = (qop_file){
|
|
.hash = hash,
|
|
.offset = w->size,
|
|
.size = (unsigned int)data_len,
|
|
.path_len = (unsigned short)path_len,
|
|
.flags = QOP_FLAG_NONE
|
|
};
|
|
|
|
w->size += data_len + path_len;
|
|
w->len++;
|
|
|
|
JS_FreeCString(js, path);
|
|
return JS_NULL;
|
|
}
|
|
|
|
static JSValue js_writer_finalize(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
|
qop_writer *w = js2writer(js, self);
|
|
if (!w || !w->fh) return JS_ThrowInternalError(js, "Invalid QOP writer or already closed");
|
|
|
|
unsigned int total_size = w->size + 12; // Header size
|
|
|
|
for (int i = 0; i < w->len; i++) {
|
|
write_64(w->files[i].hash, w->fh);
|
|
write_32(w->files[i].offset, w->fh);
|
|
write_32(w->files[i].size, w->fh);
|
|
write_16(w->files[i].path_len, w->fh);
|
|
write_16(w->files[i].flags, w->fh);
|
|
total_size += 20;
|
|
}
|
|
|
|
// Magic "qopf"
|
|
unsigned int magic = (((unsigned int)'q') << 0 | ((unsigned int)'o') << 8 |
|
|
((unsigned int)'p') << 16 | ((unsigned int)'f') << 24);
|
|
|
|
write_32(w->len, w->fh);
|
|
write_32(total_size, w->fh);
|
|
write_32(magic, w->fh);
|
|
|
|
fclose(w->fh);
|
|
w->fh = NULL;
|
|
|
|
return JS_NULL;
|
|
}
|
|
|
|
static const JSCFunctionListEntry js_qop_archive_funcs[] = {
|
|
JS_CFUNC_DEF("close", 0, js_qop_close),
|
|
JS_CFUNC_DEF("list", 0, js_qop_list),
|
|
JS_CFUNC_DEF("read", 1, js_qop_read),
|
|
JS_CFUNC_DEF("read_ex", 3, js_qop_read_ex),
|
|
JS_CFUNC_DEF("stat", 1, js_qop_stat),
|
|
JS_CFUNC_DEF("is_directory", 1, js_qop_is_directory),
|
|
};
|
|
|
|
static const JSCFunctionListEntry js_qop_writer_funcs[] = {
|
|
JS_CFUNC_DEF("add_file", 2, js_writer_add_file),
|
|
JS_CFUNC_DEF("finalize", 0, js_writer_finalize),
|
|
};
|
|
|
|
static const JSCFunctionListEntry js_qop_funcs[] = {
|
|
MIST_FUNC_DEF(qop, open, 1),
|
|
MIST_FUNC_DEF(qop, write, 1),
|
|
JS_PROP_INT32_DEF("FLAG_NONE", QOP_FLAG_NONE, JS_PROP_ENUMERABLE),
|
|
JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, JS_PROP_ENUMERABLE),
|
|
JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, JS_PROP_ENUMERABLE),
|
|
JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, JS_PROP_ENUMERABLE),
|
|
};
|
|
|
|
JSValue js_qop_use(JSContext *js) {
|
|
JS_NewClassID(&js_qop_archive_class_id);
|
|
JS_NewClass(JS_GetRuntime(js), js_qop_archive_class_id, &js_qop_archive_class);
|
|
JSValue archive_proto = JS_NewObject(js);
|
|
JS_SetPropertyFunctionList(js, archive_proto, js_qop_archive_funcs, countof(js_qop_archive_funcs));
|
|
JS_SetClassProto(js, js_qop_archive_class_id, archive_proto);
|
|
|
|
JS_NewClassID(&js_qop_writer_class_id);
|
|
JS_NewClass(JS_GetRuntime(js), js_qop_writer_class_id, &js_qop_writer_class);
|
|
JSValue writer_proto = JS_NewObject(js);
|
|
JS_SetPropertyFunctionList(js, writer_proto, js_qop_writer_funcs, countof(js_qop_writer_funcs));
|
|
JS_SetClassProto(js, js_qop_writer_class_id, writer_proto);
|
|
|
|
JSValue mod = JS_NewObject(js);
|
|
JS_SetPropertyFunctionList(js, mod, js_qop_funcs, countof(js_qop_funcs));
|
|
return mod;
|
|
} |