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
461 lines
15 KiB
C
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;
|
|
}
|