This commit is contained in:
2025-11-23 11:20:59 -06:00
parent 2beb369af9
commit 8bdcaf7d9d
9 changed files with 230 additions and 367 deletions

View File

@@ -255,22 +255,6 @@ else
deps += soloud_dep
endif
# QR code support (optional)
qrencode_enabled = get_option('qrencode')
if qrencode_enabled
qr_dep = dependency('qrencode', static: true, required: false)
if not qr_dep.found()
message('⚙ System qrencode not found, building subproject...')
deps += dependency('libqrencode', static:true)
else
deps += qr_dep
endif
add_project_arguments('-DHAVE_QRENCODE', language: ['c', 'cpp'])
src += 'qjs_qr.c'
else
message('⚙ QR code support disabled')
endif
link_args = link
sources = []
src += [
@@ -294,6 +278,7 @@ src += [
'wildmatch.c',
'qjs_io.c',
'qjs_fd.c',
'qjs_qop.c',
'qjs_os.c',
'qjs_actor.c',
'qjs_wota.c',

View File

@@ -954,26 +954,16 @@ int main(int argc, char **argv)
return 1;
}
/* Search for .cell directory up the tree */
char *search_dir = SDL_GetCurrentDirectory();
/* Check for .cell directory in the current directory */
char *cell_parent_dir = NULL;
struct stat st;
while (search_dir && strlen(search_dir) > 1) {
char test_path[PATH_MAX];
snprintf(test_path, sizeof(test_path), "%s/.cell", search_dir);
if (stat(test_path, &st) == 0 && S_ISDIR(st.st_mode)) {
cell_parent_dir = strdup(search_dir);
break;
}
char *last_sep = strrchr(search_dir, '/');
if (!last_sep || last_sep == search_dir) {
if (stat("/.cell", &st) == 0 && S_ISDIR(st.st_mode))
cell_parent_dir = strdup("/");
break;
}
*last_sep = '\0';
char *current_dir = SDL_GetCurrentDirectory();
char test_path[PATH_MAX];
snprintf(test_path, sizeof(test_path), "%s/.cell", current_dir);
if (stat(test_path, &st) == 0 && S_ISDIR(st.st_mode)) {
cell_parent_dir = strdup(current_dir);
}
SDL_free(current_dir);
if (cell_parent_dir) {
/* 1) Strip any trailing slash from cell_parent_dir (except if it's just "/") */
@@ -981,26 +971,18 @@ int main(int argc, char **argv)
if (proj_len > 1 && cell_parent_dir[proj_len - 1] == '/')
cell_parent_dir[proj_len - 1] = '\0';
char scriptpath[PATH_MAX];
snprintf(scriptpath, sizeof(scriptpath), "%s/scripts", cell_parent_dir);
char cellpath[PATH_MAX];
snprintf(cellpath, sizeof(cellpath), "%s/.cell/modules", cell_parent_dir);
PHYSFS_mount(scriptpath, NULL, 1);
PHYSFS_mount(cellpath, NULL, 0);
PHYSFS_mount(cell_parent_dir, NULL, 0);
PHYSFS_setWriteDir(cell_parent_dir);
free(cell_parent_dir);
} else {
// Not in a project - use CELLPATH after confirming ..
// TODO: implement
printf("Could not find project! Exiting!\n");
printf("Cell requires a .cell folder to run.\n");
exit(1);
}
SDL_free(search_dir);
/* Create the initial actor from the command line */
int actor_argc = argc - script_start;

View File

@@ -48,6 +48,3 @@
#define QOI_IMPLEMENTATION
#include "qoi.h"
#define QOP_IMPLEMENTATION
#include "qop.h"

View File

@@ -29,9 +29,6 @@
#include "qjs_nota.h"
#include "qjs_wota.h"
#include "qjs_soloud.h"
#ifdef HAVE_QRENCODE
#include "qjs_qr.h"
#endif
#include "qjs_sdl.h"
#include "qjs_sdl_video.h"
#include "qjs_math.h"
@@ -55,6 +52,8 @@
#include "qjs_io.h"
#include "qjs_fd.h"
#include "qjs_qop.h"
// External transform function declarations
extern JSClassID js_transform_id;
JSValue transform2js(JSContext *js, transform *t);
@@ -1123,11 +1122,8 @@ void ffi_load(JSContext *js)
// extra
arrput(rt->module_registry, ((ModuleEntry){"io", js_io_use}));
arrput(rt->module_registry, ((ModuleEntry){"fd", js_fd_use}));
arrput(rt->module_registry, MISTLINE(socket));
arrput(rt->module_registry, ((ModuleEntry){"qop", js_qop_use}));
arrput(rt->module_registry, ((ModuleEntry){"os", js_os_use}));
#ifdef HAVE_QRENCODE
arrput(rt->module_registry, MISTLINE(qr));
#endif
arrput(rt->module_registry, MISTLINE(http));
arrput(rt->module_registry, MISTLINE(crypto));
arrput(rt->module_registry, MISTLINE(miniz));

205
source/qjs_qop.c Normal file
View File

@@ -0,0 +1,205 @@
#include "qjs_qop.h"
#define QOP_IMPLEMENTATION
#include "qop.h"
#include "qjs_blob.h"
#include "jsffi.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 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_SCALL(qop_open,
qop_desc *qop = js_malloc(js, sizeof(qop_desc));
if (!qop)
ret = JS_ThrowOutOfMemory(js);
int size = qop_open(str, qop);
if (size == 0) {
js_free(js, qop);
ret = JS_ThrowReferenceError(js, "Failed to open QOP archive: %s", str);
} else {
JSValue obj = JS_NewObjectClass(js, js_qop_archive_class_id);
JS_SetOpaque(obj, qop);
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 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),
};
static const JSCFunctionListEntry js_qop_funcs[] = {
MIST_FUNC_DEF(qop, open, 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);
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_qop_funcs, countof(js_qop_funcs));
return mod;
}

8
source/qjs_qop.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef QJS_QOP_H
#define QJS_QOP_H
#include "cell.h"
JSValue js_qop_use(JSContext *js);
#endif // QJS_QOP_H

View File

@@ -1,302 +0,0 @@
#include "cell.h"
#include "qjs_blob.h"
#include "quirc.h"
#include "qrencode.h"
#include <math.h> // for sqrt
#include <stdlib.h> // for size_t, etc.
#include <string.h> // for memcpy, strcmp
#include <stdint.h>
#include <errno.h>
// QR encode function
static JSValue js_qr_encode(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(js, "encode expects a string or ArrayBuffer as first argument");
}
// Default options
int version = 0; // 0 means auto-select in qrencode
int errc = QR_ECLEVEL_L; // Default to "L"
int casesensitive = 1; // Default to true (qrencode treats 1 as case-sensitive)
int use_byte_mode = 0;
// Handle options object (argv[1])
if (argc > 1 && !JS_IsNull(argv[1]) && JS_IsObject(argv[1])) {
JSValue opt = argv[1];
JSValue m = JS_GetPropertyStr(js, opt, "mode");
if (!JS_IsNull(m)) {
const char *smode = JS_ToCString(js, m);
if (!smode) { JS_FreeValue(js, m); return JS_EXCEPTION; }
if (!strcasecmp(smode, "byte") || !strcasecmp(smode, "binary"))
use_byte_mode = 1;
else if (!strcasecmp(smode, "text"))
use_byte_mode = 0;
else {
JS_FreeCString(js, smode); JS_FreeValue(js, m);
return JS_ThrowRangeError(js, "mode must be 'byte' or 'text'");
}
JS_FreeCString(js, smode);
JS_FreeValue(js, m);
}
// Version (1-40)
JSValue v = JS_GetPropertyStr(js, opt, "version");
if (!JS_IsNull(v)) {
int32_t ver;
if (JS_ToInt32(js, &ver, v) || ver < 0 || ver > 40) {
JS_FreeValue(js, v);
return JS_ThrowRangeError(js, "version must be between 0 and 40");
}
version = ver;
JS_FreeValue(js, v);
}
// Error correction level ("l", "m", "q", "h")
JSValue l = JS_GetPropertyStr(js, opt, "level");
if (!JS_IsNull(l)) {
const char *level = JS_ToCString(js, l);
if (!level) {
JS_FreeValue(js, l);
return JS_ThrowTypeError(js, "level must be a string");
}
if (strcasecmp(level, "l") == 0) errc = QR_ECLEVEL_L;
else if (strcasecmp(level, "m") == 0) errc = QR_ECLEVEL_M;
else if (strcasecmp(level, "q") == 0) errc = QR_ECLEVEL_Q;
else if (strcasecmp(level, "h") == 0) errc = QR_ECLEVEL_H;
else {
JS_FreeCString(js, level);
JS_FreeValue(js, l);
return JS_ThrowRangeError(js, "level must be 'l', 'm', 'q', or 'h'");
}
JS_FreeCString(js, level);
JS_FreeValue(js, l);
}
// Case sensitivity (true/false)
JSValue ci = JS_GetPropertyStr(js, opt, "caseinsensitive");
if (!JS_IsNull(ci)) {
int bool_val;
if (JS_ToBool(js, ci)) {
bool_val = 0; // qrencode: 0 = case-insensitive
} else {
bool_val = 1; // qrencode: 1 = case-sensitive
}
casesensitive = bool_val;
JS_FreeValue(js, ci);
}
}
const char *data = NULL;
size_t data_len = 0;
int must_free_data = 0;
// Handle string input
if (!use_byte_mode && JS_IsString(argv[0])) {
data = JS_ToCStringLen(js, &data_len, argv[0]);
if (!data) {
return JS_ThrowTypeError(js, "Invalid string input");
}
must_free_data = 1;
} else {
/* treat everything else as raw bytes */
use_byte_mode = 1;
uint8_t *buf = js_get_blob_data(js, &data_len, argv[0]);
if (!buf) {
return JS_ThrowTypeError(js, "encode expects a string or ArrayBuffer");
}
data = (const char *)buf; // Cast to char* for qrencode
}
// Encode using qrencode
QRcode *qrcode;
if (use_byte_mode)
qrcode = QRcode_encodeData(data_len, data, version, errc);
else
qrcode = QRcode_encodeString(data, version, errc, QR_MODE_8, casesensitive);
if (!qrcode) {
if (must_free_data)
JS_FreeCString(js, data);
return JS_ThrowInternalError(js, "Failed to encode QR code: %s", strerror(errno));
}
// qrcode->width is the size of one side
// qrcode->data is width * width bytes
printf("Encoded %d of data into a QR of width %d\n", data_len, qrcode->width);
int width = qrcode->width;
size_t size = (size_t)width * width; // total modules
// Create an array of booleans for the module data
JSValue dataArr = JS_NewArray(js);
for (size_t i = 0; i < size; i++) {
int is_black = (qrcode->data[i] & 1);
JSValue val = JS_NewBool(js, is_black);
JS_SetPropertyUint32(js, dataArr, (uint32_t)i, val);
}
// Build a JS object to hold { width, data }
JSValue result = JS_NewObject(js);
JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, result, "data", dataArr);
// Clean up
QRcode_free(qrcode);
if (must_free_data) {
JS_FreeCString(js, data);
}
return result;
}
static uint8_t rgba_to_gray(uint8_t r, uint8_t g, uint8_t b)
{
return (uint8_t)(( 299 * r + 587 * g + 114 * b + 500) / 1000);
}
// QR decode function (unchanged)
static JSValue js_qr_decode(JSContext *js, JSValueConst this_val, int argc, JSValueConst *argv) {
size_t data_len;
uint8_t *src;
int w, h, pitch;
src = js_get_blob_data(js, &data_len, argv[0]);
if (!src)
return JS_ThrowTypeError(js, "decode expects an ArrayBuffer");
JS_ToInt32(js, &w, argv[1]);
JS_ToInt32(js, &h, argv[2]);
JS_ToInt32(js, &pitch, argv[3]);
if (w <= 0 || h <= 0)
return JS_ThrowInternalError(js, "Bad width or height: %dx%d", w, h);
struct quirc *qr = quirc_new();
if (!qr)
return JS_ThrowInternalError(js, "Failed to initialize QR decoder");
if (quirc_resize(qr, w, h) < 0) {
quirc_destroy(qr);
return JS_ThrowInternalError(js, "quirc_resize failed");
}
uint8_t *dst = quirc_begin(qr, NULL, NULL);
printf("decoding image size %dx%d, pitch %d and size %d\n", w, h, pitch, data_len);
for (int y = 0; y < h; ++y) {
uint8_t *row = src + y * pitch;
uint8_t *out = dst + y * w;
for (int x = 0; x < w; ++x) {
uint8_t a = row[x*4 + 3]; /* alpha */
if (a < 128) { /* mostly transparent */
out[x] = 255;
} else {
uint8_t r = row[x*4+0];
uint8_t g = row[x*4+1];
uint8_t b = row[x*4+2];
out[x] = rgba_to_gray(r, g, b);
}
}
}
quirc_end(qr);
int count = quirc_count(qr);
JSValue result = JS_NewArray(js);
for (int i = 0; i < count; i++) {
struct quirc_code code;
struct quirc_data qdata;
quirc_extract(qr, i, &code);
int err = quirc_decode(&code, &qdata);
if (err == QUIRC_SUCCESS) {
JSValue item = js_new_blob_stoned_copy(js, qdata.payload, qdata.payload_len);
JS_SetPropertyUint32(js, result, i, item);
} else {
printf("QR error: %s\n", quirc_strerror(err));
}
}
quirc_destroy(qr);
return result;
}
static JSValue js_qr_rgba(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
if (argc < 1)
return JS_ThrowTypeError(js, "qr.rgba(mods, ...) needs a modules object");
JSValue mods = argv[0];
uint32_t on = 0xFF000000u; /* black opaque */
uint32_t off = 0xFFFFFFFFu; /* white opaque */
if (argc > 1) JS_ToUint32(js, &on, argv[1]);
if (argc > 2) JS_ToUint32(js, &off, argv[2]);
JSValue js_w = JS_GetPropertyStr(js, mods, "width");
if (JS_IsNull(js_w)) {
JS_FreeValue(js, js_w);
return JS_ThrowTypeError(js, "mods.width missing");
}
int32_t m = 0;
JS_ToInt32(js, &m, js_w);
JS_FreeValue(js, js_w);
if (m <= 0)
return JS_ThrowRangeError(js, "mods.width must be > 0");
JSValue js_bits = JS_GetPropertyStr(js, mods, "data");
size_t len = JS_ArrayLength(js, js_bits);
if (len < (size_t)m * (size_t)m) {
JS_FreeValue(js, js_bits);
return JS_ThrowRangeError(js, "mods.data too small for width²");
}
const int w = m, h = m, pitch = w * 4;
const size_t bytes = (size_t)h * (size_t)pitch;
uint8_t *tmp = js_malloc(js, bytes);
if (!tmp) { JS_FreeValue(js, js_bits); return JS_EXCEPTION; }
for (int y = 0; y < h; ++y) {
uint32_t *row = (uint32_t *)(tmp + y * pitch);
for (int x = 0; x < w; x++) {
uint32_t idx = y * m + x;
JSValue vbit = JS_GetPropertyUint32(js, js_bits, idx);
int on_bit = JS_ToBool(js, vbit);
JS_FreeValue(js, vbit);
row[x] = on_bit ? on : off;
}
}
JSValue ab = js_new_blob_stoned_copy(js, tmp, bytes); /* GC owns copy */
js_free(js, tmp);
JS_FreeValue(js, js_bits); /* done with src */
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, w));
JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, h));
JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, pitch));
JS_SetPropertyStr(js, obj, "buffer", ab);
JS_SetPropertyStr(js, obj, "format", JS_NewString(js, "rgba32"));
return obj;
}
// Exported functions for the module
static const JSCFunctionListEntry js_qr_funcs[] = {
JS_CFUNC_DEF("encode", 2, js_qr_encode), // Updated to expect 2 args
JS_CFUNC_DEF("decode", 4, js_qr_decode),
JS_CFUNC_DEF("rgba", 3, js_qr_rgba),
};
// Helper to return the default export object
JSValue js_qr_use(JSContext *js) {
JSValue exports = JS_NewObject(js);
JS_SetPropertyFunctionList(js, exports, js_qr_funcs, sizeof(js_qr_funcs) / sizeof(JSCFunctionListEntry));
return exports;
}

View File

@@ -1,8 +0,0 @@
#ifndef QJS_QR_H
#define QJS_QR_H
#include "cell.h"
JSValue js_qr_use(JSContext*);
#endif

View File

@@ -84,7 +84,7 @@ typedef struct {
// Open an archive at path. The supplied qop_desc will be filled with the
// information from the file header. Returns the size of the archvie or 0 on
// failure
// failure.
int qop_open(const char *path, qop_desc *qop);
// Read the index from an opened archive. The supplied buffer will be filled
@@ -94,10 +94,10 @@ int qop_open(const char *path, qop_desc *qop);
// Returns the number of files in the archive or 0 on error.
int qop_read_index(qop_desc *qop, void *buffer);
// Close the archive
// Close the archive.
void qop_close(qop_desc *qop);
// Find a file with the supplied path. Returns NULL if the file is not found
// Find a file with the supplied path. Returns NULL if the file is not found.
qop_file *qop_find(qop_desc *qop, const char *path);
// Copy the path of the file into dest. The dest buffer must be at least
@@ -107,7 +107,7 @@ int qop_read_path(qop_desc *qop, qop_file *file, char *dest);
// Read the whole file into dest. The dest buffer must be at least file->size
// bytes long.
// Returns the number of bytes read
// Returns the number of bytes read.
int qop_read(qop_desc *qop, qop_file *file, unsigned char *dest);
// Read part of a file into dest. The dest buffer must be at least len bytes
@@ -279,4 +279,4 @@ int qop_read_ex(qop_desc *qop, qop_file *file, unsigned char *dest, unsigned int
}
#endif /* QOP_IMPLEMENTATION */
#endif /* QOP_IMPLEMENTATION */