Files
cell/scripts/qop.c
2025-12-04 20:15:32 -06:00

484 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 == -1)
ret = JS_EXCEPTION;
else if (!data)
ret = JS_ThrowReferenceError(js, "Empty blob");
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 == (void*)-1) {
JS_FreeCString(js, path);
return JS_EXCEPTION;
}
if (!data) {
JS_FreeCString(js, path);
return JS_ThrowTypeError(js, "No blob data present");
}
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;
}