qopconv; now uses cell's own qopconv for building

This commit is contained in:
2025-11-25 12:25:28 -06:00
parent 67badc3e48
commit 9b19d19698
12 changed files with 787 additions and 245 deletions

View File

@@ -79,7 +79,7 @@ JSC_SCALL(fd_open,
int fd = open(str, flags, mode);
if (fd < 0)
ret = JS_ThrowReferenceError(js, "open failed: %s", strerror(errno));
ret = JS_ThrowInternalError(js, "open failed: %s", strerror(errno));
else
ret = JS_NewInt32(js, fd);
)
@@ -90,7 +90,7 @@ JSC_CCALL(fd_write,
ssize_t wrote = js_fd_write_helper(js, fd, argv[1]);
if (wrote < 0)
return JS_ThrowReferenceError(js, "write failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "write failed: %s", strerror(errno));
return JS_NewInt64(js, wrote);
)
@@ -105,12 +105,12 @@ JSC_CCALL(fd_read,
void *buf = malloc(size);
if (!buf)
return JS_ThrowReferenceError(js, "malloc failed");
return JS_ThrowInternalError(js, "malloc failed");
ssize_t bytes_read = read(fd, buf, size);
if (bytes_read < 0) {
free(buf);
return JS_ThrowReferenceError(js, "read failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "read failed: %s", strerror(errno));
}
ret = js_new_blob_stoned_copy(js, buf, bytes_read);
@@ -121,7 +121,7 @@ JSC_CCALL(fd_read,
JSC_SCALL(fd_slurp,
struct stat st;
if (stat(str, &st) != 0)
return JS_ThrowReferenceError(js, "stat failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "stat failed: %s", strerror(errno));
if (!S_ISREG(st.st_mode))
return JS_ThrowTypeError(js, "path is not a regular file");
@@ -133,12 +133,12 @@ JSC_SCALL(fd_slurp,
#ifndef _WIN32
int fd = open(str, O_RDONLY);
if (fd < 0)
return JS_ThrowReferenceError(js, "open failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "open failed: %s", strerror(errno));
void *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) {
close(fd);
return JS_ThrowReferenceError(js, "mmap failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "mmap failed: %s", strerror(errno));
}
ret = js_new_blob_stoned_copy(js, data, size);
munmap(data, size);
@@ -147,19 +147,19 @@ JSC_SCALL(fd_slurp,
// Windows: use memory mapping for optimal performance
HANDLE hFile = CreateFileA(str, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return JS_ThrowReferenceError(js, "CreateFile failed: %lu", GetLastError());
return JS_ThrowInternalError(js, "CreateFile failed: %lu", GetLastError());
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapping == NULL) {
CloseHandle(hFile);
return JS_ThrowReferenceError(js, "CreateFileMapping failed: %lu", GetLastError());
return JS_ThrowInternalError(js, "CreateFileMapping failed: %lu", GetLastError());
}
void *data = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (data == NULL) {
CloseHandle(hMapping);
CloseHandle(hFile);
return JS_ThrowReferenceError(js, "MapViewOfFile failed: %lu", GetLastError());
return JS_ThrowInternalError(js, "MapViewOfFile failed: %lu", GetLastError());
}
ret = js_new_blob_stoned_copy(js, data, size);
@@ -185,7 +185,7 @@ JSC_CCALL(fd_lseek,
off_t new_pos = lseek(fd, offset, whence);
if (new_pos < 0)
return JS_ThrowReferenceError(js, "lseek failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "lseek failed: %s", strerror(errno));
return JS_NewInt64(js, new_pos);
)
@@ -193,23 +193,90 @@ JSC_CCALL(fd_lseek,
JSC_CCALL(fd_getcwd,
char buf[PATH_MAX];
if (getcwd(buf, sizeof(buf)) == NULL)
return JS_ThrowReferenceError(js, "getcwd failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "getcwd failed: %s", strerror(errno));
return JS_NewString(js, buf);
)
JSC_SCALL(fd_rmdir,
if (rmdir(str) != 0)
ret = JS_ThrowReferenceError(js, "could not remove directory %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "could not remove directory %s: %s", str, strerror(errno));
)
JSC_SCALL(fd_mkdir,
if (mkdir(str, 0755) != 0)
ret = JS_ThrowReferenceError(js, "could not make directory %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "could not make directory %s: %s", str, strerror(errno));
)
JSC_SCALL(fd_unlink,
if (unlink(str) != 0)
ret = JS_ThrowReferenceError(js, "could not remove file %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "could not remove file %s: %s", str, strerror(errno));
)
JSC_SCALL(fd_mv,
if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsString(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
else {
const char *new_path = JS_ToCString(js, argv[1]);
if (rename(str, new_path) != 0)
ret = JS_ThrowInternalError(js, "could not rename %s to %s: %s", str, new_path, strerror(errno));
JS_FreeCString(js, new_path);
}
)
// Helper function for recursive removal
static int remove_recursive(const char *path) {
struct stat st;
if (stat(path, &st) != 0)
return -1;
if (S_ISDIR(st.st_mode)) {
// Directory: remove contents first
#ifdef _WIN32
WIN32_FIND_DATA ffd;
char search_path[PATH_MAX];
snprintf(search_path, sizeof(search_path), "%s\\*", path);
HANDLE hFind = FindFirstFile(search_path, &ffd);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
char child_path[PATH_MAX];
snprintf(child_path, sizeof(child_path), "%s\\%s", path, ffd.cFileName);
if (remove_recursive(child_path) != 0) {
FindClose(hFind);
return -1;
}
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
}
#else
DIR *d = opendir(path);
if (d) {
struct dirent *dir;
while ((dir = readdir(d)) != NULL) {
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
char child_path[PATH_MAX];
snprintf(child_path, sizeof(child_path), "%s/%s", path, dir->d_name);
if (remove_recursive(child_path) != 0) {
closedir(d);
return -1;
}
}
closedir(d);
}
#endif
// Remove the now-empty directory
return rmdir(path);
} else {
// File: just unlink
return unlink(path);
}
}
JSC_SCALL(fd_rm,
if (remove_recursive(str) != 0)
ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, strerror(errno));
)
JSC_CCALL(fd_fsync,
@@ -217,7 +284,7 @@ JSC_CCALL(fd_fsync,
if (fd < 0) return JS_EXCEPTION;
if (fsync(fd) != 0)
return JS_ThrowReferenceError(js, "fsync failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "fsync failed: %s", strerror(errno));
return JS_NULL;
)
@@ -227,7 +294,7 @@ JSC_CCALL(fd_close,
if (fd < 0) return JS_EXCEPTION;
if (close(fd) != 0)
return JS_ThrowReferenceError(js, "close failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "close failed: %s", strerror(errno));
return JS_NULL;
)
@@ -238,7 +305,7 @@ JSC_CCALL(fd_fstat,
struct stat st;
if (fstat(fd, &st) != 0)
return JS_ThrowReferenceError(js, "fstat failed: %s", strerror(errno));
return JS_ThrowInternalError(js, "fstat failed: %s", strerror(errno));
printf("fstat on %s\n", argv[0]);
JSValue obj = JS_NewObject(js);
@@ -317,7 +384,7 @@ JSC_SCALL(fd_readdir,
snprintf(path, sizeof(path), "%s\\*", str);
HANDLE hFind = FindFirstFile(path, &ffd);
if (hFind == INVALID_HANDLE_VALUE) {
ret = JS_ThrowReferenceError(js, "FindFirstFile failed for %s", path);
ret = JS_ThrowInternalError(js, "FindFirstFile failed for %s", path);
} else {
ret = JS_NewArray(js);
int i = 0;
@@ -340,7 +407,7 @@ JSC_SCALL(fd_readdir,
}
closedir(d);
} else {
ret = JS_ThrowReferenceError(js, "opendir failed for %s: %s", str, strerror(errno));
ret = JS_ThrowInternalError(js, "opendir failed for %s: %s", str, strerror(errno));
}
#endif
)
@@ -355,6 +422,8 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, rmdir, 1),
MIST_FUNC_DEF(fd, unlink, 1),
MIST_FUNC_DEF(fd, mkdir, 1),
MIST_FUNC_DEF(fd, mv, 2),
MIST_FUNC_DEF(fd, rm, 1),
MIST_FUNC_DEF(fd, fsync, 1),
MIST_FUNC_DEF(fd, close, 1),
MIST_FUNC_DEF(fd, stat, 1),

View File

@@ -21,6 +21,74 @@ static JSClassDef js_qop_archive_class = {
.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);
}
@@ -60,6 +128,36 @@ JSC_SCALL(qop_open,
}
)
JSC_SCALL(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);
@@ -183,15 +281,179 @@ static JSValue js_qop_list(JSContext *js, JSValue self, int argc, JSValue *argv)
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),
@@ -205,6 +467,12 @@ JSValue js_qop_use(JSContext *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;