#define QOP_IMPLEMENTATION #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 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, 0), JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, 0), JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, 0), JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, 0), }; 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; }