Files
cell/source/qjs_blob.c
John Alanbrook 3a4547fb80
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
add blob; pull out crypto, time; add sdl_renderer
2025-03-27 14:31:02 -05:00

461 lines
15 KiB
C

#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "quickjs.h"
#include "qjs_blob.h"
// -----------------------------------------------------------------------------
// A simple blob structure that can be in two states:
// - antestone (mutable): writing is allowed
// - stone (immutable): reading is allowed
//
// The blob is stored as an array of bits in memory, but for simplicity here,
// we store them in a dynamic byte array with a bit_length and capacity in bits.
//
// This is a minimal demonstration. Real usage might require more sophisticated
// memory or bit manipulation for performance.
// -----------------------------------------------------------------------------
typedef struct {
// The actual buffer holding the bits (in multiples of 8 bits).
uint8_t *data;
// The total number of bits currently in use (the "length" of the blob).
size_t bit_length;
// The total capacity in bits that 'data' can currently hold without realloc.
size_t bit_capacity;
// 0 = antestone (mutable)
// 1 = stone (immutable)
int is_stone;
} JSBlobData;
// Forward declaration of class ID and methods
static JSClassID js_blob_class_id;
// Helper to ensure capacity for writing
// new_bits is additional bits to be appended
static int js_blob_ensure_capacity(JSContext *ctx, JSBlobData *bd, size_t new_bits) {
size_t need_bits = bd->bit_length + new_bits;
if (need_bits <= bd->bit_capacity) return 0;
// Increase capacity (in multiples of bytes).
// We can pick a growth strategy. For demonstration, double it:
size_t new_capacity = bd->bit_capacity == 0 ? 64 : bd->bit_capacity * 2;
while (new_capacity < need_bits) new_capacity *= 2;
// Round up new_capacity to a multiple of 8 bits
if (new_capacity % 8) {
new_capacity += 8 - (new_capacity % 8);
}
size_t new_size_bytes = new_capacity / 8;
uint8_t *new_ptr = realloc(bd->data, new_size_bytes);
if (!new_ptr) {
return -1; // out of memory
}
// zero-fill the new area (only beyond the old capacity)
size_t old_size_bytes = bd->bit_capacity / 8;
if (new_size_bytes > old_size_bytes) {
memset(new_ptr + old_size_bytes, 0, new_size_bytes - old_size_bytes);
}
bd->data = new_ptr;
bd->bit_capacity = new_capacity;
return 0;
}
// Finalizer for JSBlobData
static void js_blob_finalizer(JSRuntime *rt, JSValue val) {
JSBlobData *bd = JS_GetOpaque(val, js_blob_class_id);
if (bd) {
free(bd->data);
bd->data = NULL;
bd->bit_length = 0;
bd->bit_capacity = 0;
bd->is_stone = 0;
free(bd);
}
}
// Mark function: not used here, as we have no child JS objects in JSBlobData
static void js_blob_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) {
// No child JS references to mark
}
// A helper to create a new JSBlobData object, returning a JSValue wrapping it.
static JSValue js_blob_wrap(JSContext *ctx, JSBlobData *bd) {
JSValue obj = JS_NewObjectClass(ctx, js_blob_class_id);
if (JS_IsException(obj)) {
free(bd->data);
free(bd);
return obj;
}
JS_SetOpaque(obj, bd);
return obj;
}
// -----------------------------------------------------------------------------
// Helpers for reading/writing bits
// -----------------------------------------------------------------------------
// Write one bit (0 or 1) at the end of the blob
static int js_blob_write_bit_internal(JSContext *ctx, JSBlobData *bd, int bit_val) {
if (bd->is_stone) {
// Trying to write to an immutable blob -> throw
return -1;
}
if (js_blob_ensure_capacity(ctx, bd, 1) < 0) {
return -1;
}
// index in bits
size_t bit_index = bd->bit_length;
size_t byte_index = bit_index >> 3;
size_t offset_in_byte = bit_index & 7;
// set or clear bit
if (bit_val)
bd->data[byte_index] |= (1 << offset_in_byte);
else
bd->data[byte_index] &= ~(1 << offset_in_byte);
bd->bit_length++;
return 0;
}
// Read one bit from a stone blob at position 'pos'
static int js_blob_read_bit_internal(JSBlobData *bd, size_t pos, int *out_bit) {
if (!bd->is_stone) {
// It's not stone -> reading might be out of the specification
// but we can allow or return error. Here we just return error.
return -1;
}
if (pos >= bd->bit_length) {
return -1; // out of range
}
size_t byte_index = pos >> 3;
size_t offset_in_byte = pos & 7;
*out_bit = (bd->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0;
return 0;
}
// Turn a blob into the "stone" state. This discards any extra capacity.
static void js_blob_make_stone(JSBlobData *bd) {
bd->is_stone = 1;
// Optionally shrink the buffer to exactly bit_length in size
if (bd->bit_capacity > bd->bit_length) {
size_t size_in_bytes = (bd->bit_length + 7) >> 3; // round up to full bytes
uint8_t *new_ptr = NULL;
if (size_in_bytes) {
new_ptr = realloc(bd->data, size_in_bytes);
if (new_ptr) {
bd->data = new_ptr;
}
} else {
// zero length
free(bd->data);
bd->data = NULL;
}
bd->bit_capacity = bd->bit_length; // capacity in bits now matches length
}
}
// -----------------------------------------------------------------------------
// JS Functions (blob.make, blob.write_bit, blob.read_logical, etc.)
// -----------------------------------------------------------------------------
// blob.make(...)
static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
// We'll implement a few typical signatures:
// blob.make()
// blob.make(capacity)
// blob.make(length, logical_value)
// blob.make(blob, from, to) (makes a copy)
//
// This is a simplified approach. The spec mentions random, partial copy, etc.
// We'll handle just these forms enough to demonstrate the concept.
JSBlobData *bd = calloc(1, sizeof(*bd));
if (!bd) return JS_ThrowOutOfMemory(ctx);
// default
bd->data = NULL;
bd->bit_length = 0;
bd->bit_capacity = 0;
bd->is_stone = 0; // initially antestone
// blob.make()
if (argc == 0) {
// empty antestone blob
}
// blob.make(capacity)
else if (argc == 1 && JS_IsNumber(argv[0])) {
int64_t capacity_bits;
if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) {
free(bd);
return JS_EXCEPTION;
}
if (capacity_bits < 0) capacity_bits = 0;
bd->bit_capacity = (size_t)capacity_bits;
if (bd->bit_capacity % 8) {
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
}
if (bd->bit_capacity) {
size_t bytes = bd->bit_capacity / 8;
bd->data = calloc(bytes, 1);
if (!bd->data) {
free(bd);
return JS_ThrowOutOfMemory(ctx);
}
}
}
// blob.make(length, logical)
else if (argc == 2 && JS_IsNumber(argv[0]) && JS_IsBool(argv[1])) {
int64_t length_bits;
if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) {
free(bd);
return JS_EXCEPTION;
}
if (length_bits < 0) length_bits = 0;
int is_one = JS_ToBool(ctx, argv[1]);
bd->bit_length = (size_t)length_bits;
bd->bit_capacity = bd->bit_length;
if (bd->bit_capacity % 8) {
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
}
size_t bytes = bd->bit_capacity / 8;
if (bytes) {
bd->data = malloc(bytes);
if (!bd->data) {
free(bd);
return JS_ThrowOutOfMemory(ctx);
}
memset(bd->data, is_one ? 0xff : 0x00, bytes);
// if length_bits isn't a multiple of 8, we need to clear the unused bits
size_t used_bits_in_last_byte = (size_t)length_bits & 7;
if (used_bits_in_last_byte && is_one) {
// clear top bits in the last byte
uint8_t mask = (1 << used_bits_in_last_byte) - 1;
bd->data[bytes - 1] &= mask;
}
}
}
// blob.make(blob, from, to)
else if (argc >= 1 && JS_IsObject(argv[0])) {
// we try copying from another blob if it's of the same class
JSBlobData *src = JS_GetOpaque(argv[0], js_blob_class_id);
if (!src) {
free(bd);
return JS_ThrowTypeError(ctx, "blob.make: argument 1 not a blob");
}
int64_t from = 0, to = (int64_t)src->bit_length;
if (argc >= 2 && JS_IsNumber(argv[1])) {
JS_ToInt64(ctx, &from, argv[1]);
if (from < 0) from = 0;
}
if (argc >= 3 && JS_IsNumber(argv[2])) {
JS_ToInt64(ctx, &to, argv[2]);
if (to < from) to = from;
if (to > (int64_t)src->bit_length) to = (int64_t)src->bit_length;
}
size_t copy_len = (size_t)(to - from);
bd->bit_length = copy_len;
bd->bit_capacity = copy_len;
if (bd->bit_capacity % 8) {
bd->bit_capacity += 8 - (bd->bit_capacity % 8);
}
size_t bytes = bd->bit_capacity / 8;
if (bytes) {
bd->data = calloc(bytes, 1);
if (!bd->data) {
free(bd);
return JS_ThrowOutOfMemory(ctx);
}
}
// Now copy the bits.
// For simplicity, let's do a naive bit copy one by one:
for (size_t i = 0; i < copy_len; i++) {
size_t src_bit_index = from + i;
size_t src_byte = src_bit_index >> 3;
size_t src_off = src_bit_index & 7;
int bit_val = (src->data[src_byte] >> src_off) & 1;
size_t dst_byte = i >> 3;
size_t dst_off = i & 7;
if (bit_val) {
bd->data[dst_byte] |= (1 << dst_off);
} else {
bd->data[dst_byte] &= ~(1 << dst_off);
}
}
}
// else fail
else {
free(bd);
return JS_ThrowTypeError(ctx, "blob.make: invalid arguments");
}
return js_blob_wrap(ctx, bd);
}
// blob.write_bit(blob, logical)
static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2) {
return JS_ThrowTypeError(ctx, "blob.write_bit(blob, logical) requires 2 arguments");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.write_bit: argument 1 not a blob");
}
int bit_val = JS_ToBool(ctx, argv[1]); // interpret any truthy as 1, else 0
if (js_blob_write_bit_internal(ctx, bd, bit_val) < 0) {
return JS_ThrowTypeError(ctx, "blob.write_bit: cannot write (maybe stone or OOM)");
}
return JS_UNDEFINED;
}
// blob.read_logical(blob, from)
static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 2) {
return JS_ThrowTypeError(ctx, "blob.read_logical(blob, from) requires 2 arguments");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.read_logical: argument 1 not a blob");
}
int64_t pos;
if (JS_ToInt64(ctx, &pos, argv[1]) < 0) {
return JS_EXCEPTION;
}
if (pos < 0) {
return JS_NULL; // out of range
}
int bit_val;
if (js_blob_read_bit_internal(bd, (size_t)pos, &bit_val) < 0) {
return JS_NULL; // error or out of range
}
return JS_NewBool(ctx, bit_val);
}
// blob.stone(blob)
static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "blob.stone(blob) requires 1 argument");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.stone: argument not a blob");
}
if (!bd->is_stone) {
js_blob_make_stone(bd);
}
return JS_UNDEFINED;
}
// blob.length(blob)
// Return number of bits in the blob
static JSValue js_blob_length(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) {
return JS_ThrowTypeError(ctx, "blob.length(blob) requires 1 argument");
}
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
if (!bd) {
return JS_ThrowTypeError(ctx, "blob.length: argument not a blob");
}
return JS_NewInt64(ctx, bd->bit_length);
}
// blob.blob?(value)
// Return true if the value is a blob object
static JSValue js_blob_is_blob(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
if (argc < 1) return JS_FALSE;
JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id);
return JS_NewBool(ctx, bd != NULL);
}
// -----------------------------------------------------------------------------
// Exports list
// -----------------------------------------------------------------------------
static const JSCFunctionListEntry js_blob_funcs[] = {
// The "make" function.
JS_CFUNC_DEF("make", 3, js_blob_make),
// Some example read/write routines
JS_CFUNC_DEF("write_bit", 2, js_blob_write_bit),
JS_CFUNC_DEF("read_logical", 2, js_blob_read_logical),
// Convert blob from antestone -> stone
JS_CFUNC_DEF("stone", 1, js_blob_stone),
// Return the length in bits
JS_CFUNC_DEF("length", 1, js_blob_length),
// Check if a value is a blob
JS_CFUNC_DEF("isblob", 1, js_blob_is_blob),
};
// -----------------------------------------------------------------------------
// Class definition for the 'blob' objects
// -----------------------------------------------------------------------------
static JSClassDef js_blob_class = {
"BlobClass",
.finalizer = js_blob_finalizer,
.gc_mark = js_blob_mark,
};
// Module init function
static int js_blob_init(JSContext *ctx, JSModuleDef *m) {
// Register the class if not already done
if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) {
JS_NewClassID(&js_blob_class_id);
JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class);
}
// Create a prototype object
JSValue proto = JS_NewObject(ctx);
JS_SetClassProto(ctx, js_blob_class_id, proto);
// Export our functions as named exports
JS_SetModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
return 0;
}
// The module entry point
#ifdef JS_SHARED_LIBRARY
#define JS_INIT_MODULE js_init_module
#else
#define JS_INIT_MODULE js_init_module_blob
#endif
JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) {
JSModuleDef *m = JS_NewCModule(ctx, module_name, js_blob_init);
if (!m) return NULL;
JS_AddModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
return m;
}
// -----------------------------------------------------------------------------
// js_blob_use(ctx) for easy embedding: returns an object with the blob functions
// -----------------------------------------------------------------------------
JSValue js_blob_use(JSContext *ctx) {
// Ensure class is registered
if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) {
JS_NewClassID(&js_blob_class_id);
JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class);
// Create a prototype object
JSValue proto = JS_NewObject(ctx);
JS_SetClassProto(ctx, js_blob_class_id, proto);
}
// Create a plain object (the "exports") and add the funcs
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, obj, js_blob_funcs,
sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0]));
return obj;
}