Merge branch 'master' into fix_aot
This commit is contained in:
@@ -8,7 +8,7 @@ function use_embed(name) {
|
||||
|
||||
var fd = use_embed('internal_fd')
|
||||
var json_mod = use_embed('json')
|
||||
var crypto = use_embed('crypto')
|
||||
var crypto = use_embed('internal_crypto')
|
||||
|
||||
function content_hash(content) {
|
||||
var data = content
|
||||
@@ -34,7 +34,7 @@ function boot_load(name) {
|
||||
var mcode_blob = null
|
||||
var mach_blob = null
|
||||
if (!fd.is_file(mcode_path)) {
|
||||
print("error: missing seed: " + name + "\n")
|
||||
os.print("error: missing seed: " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
@@ -60,9 +60,9 @@ function analyze(src, filename) {
|
||||
e = ast.errors[_i]
|
||||
msg = e.message
|
||||
if (e.line != null && e.column != null)
|
||||
print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}`)
|
||||
os.print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}\n`)
|
||||
else
|
||||
print(`${filename}: error: ${msg}`)
|
||||
os.print(`${filename}: error: ${msg}\n`)
|
||||
_i = _i + 1
|
||||
}
|
||||
disrupt
|
||||
@@ -105,4 +105,4 @@ while (_i < length(seed_files)) {
|
||||
compile_and_cache(entry.name, core_path + '/' + entry.path)
|
||||
_i = _i + 1
|
||||
}
|
||||
print("bootstrap: cache seeded\n")
|
||||
os.print("bootstrap: cache seeded\n")
|
||||
|
||||
247
internal/crypto.c
Normal file
247
internal/crypto.c
Normal file
@@ -0,0 +1,247 @@
|
||||
#include "cell.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "monocypher.h"
|
||||
|
||||
/*
|
||||
Crypto Module Documentation
|
||||
|
||||
This module provides cryptographic functions using the Monocypher library.
|
||||
All inputs and outputs are Blobs.
|
||||
|
||||
Functions:
|
||||
|
||||
- keypair() -> { public: Blob(256 bits), private: Blob(256 bits) }
|
||||
Generates a new random X25519 keypair.
|
||||
|
||||
- shared(public_key, private_key) -> Blob(256 bits)
|
||||
Computes a shared secret from your private key and another's public key (X25519).
|
||||
Input keys must be 256 bits (32 bytes).
|
||||
|
||||
- blake2(data, [hash_size_bytes=32]) -> Blob
|
||||
Computes the BLAKE2b hash of the data.
|
||||
Default hash size is 32 bytes (256 bits). Supports 1-64 bytes.
|
||||
|
||||
- sign(secret_key, message) -> Blob(512 bits)
|
||||
Signs a message using EdDSA.
|
||||
secret_key must be 512 bits (64 bytes).
|
||||
(Note: If you have a 32-byte seed, extend it first or use appropriate key generation).
|
||||
Returns a 64-byte signature.
|
||||
|
||||
- verify(signature, public_key, message) -> bool
|
||||
Verifies an EdDSA signature.
|
||||
signature: 512 bits (64 bytes).
|
||||
public_key: 256 bits (32 bytes).
|
||||
Returns true if valid, false otherwise.
|
||||
|
||||
- lock(key, nonce, message, [ad]) -> Blob
|
||||
Encrypts and authenticates a message using XChaCha20-Poly1305.
|
||||
key: 256 bits (32 bytes).
|
||||
nonce: 192 bits (24 bytes).
|
||||
ad: Optional associated data (Blob).
|
||||
Returns a blob containing the ciphertext followed by the 16-byte MAC.
|
||||
|
||||
- unlock(key, nonce, ciphertext_with_mac, [ad]) -> Blob or null
|
||||
Decrypts and verifies a message.
|
||||
key: 256 bits (32 bytes).
|
||||
nonce: 192 bits (24 bytes).
|
||||
ciphertext_with_mac: Must include the 16-byte MAC at the end.
|
||||
ad: Optional associated data (Blob).
|
||||
Returns the plaintext Blob if successful, or null if verification fails.
|
||||
*/
|
||||
|
||||
// Helper to get blob data and check exact bit length
|
||||
static void *get_blob_check_bits(JSContext *js, JSValue val, size_t expected_bits, const char *name) {
|
||||
size_t bits;
|
||||
void* result = js_get_blob_data_bits(js, &bits, val);
|
||||
if (result == (void *)-1) {
|
||||
return NULL; // Exception already thrown by js_get_blob_data_bits
|
||||
}
|
||||
|
||||
if (bits != expected_bits) {
|
||||
JS_RaiseDisrupt(js, "%s: expected %zu bits, got %zu", name, expected_bits, bits);
|
||||
return NULL;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper to get any blob data (checking it is a stoned blob)
|
||||
static void *get_blob_any(JSContext *js, JSValue val, size_t *out_bits, const char *name) {
|
||||
void *result = js_get_blob_data_bits(js, out_bits, val);
|
||||
if (result == (void *)-1)
|
||||
return NULL;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
return JS_RaiseDisrupt(js, "crypto.shared: expected public_key, private_key");
|
||||
}
|
||||
|
||||
uint8_t *pub = get_blob_check_bits(js, argv[0], 256, "crypto.shared public_key");
|
||||
if (!pub) return JS_EXCEPTION;
|
||||
|
||||
uint8_t *priv = get_blob_check_bits(js, argv[1], 256, "crypto.shared private_key");
|
||||
if (!priv) return JS_EXCEPTION;
|
||||
|
||||
uint8_t shared[32];
|
||||
crypto_x25519(shared, priv, pub);
|
||||
|
||||
return js_new_blob_stoned_copy(js, shared, 32);
|
||||
}
|
||||
|
||||
JSValue js_crypto_blake2(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
if (argc < 1)
|
||||
return JS_RaiseDisrupt(js, "crypto.blake2: expected data blob");
|
||||
|
||||
size_t data_bits;
|
||||
uint8_t *data = get_blob_any(js, argv[0], &data_bits, "crypto.blake2 data");
|
||||
if (!data) return JS_EXCEPTION;
|
||||
|
||||
int32_t hash_len = 32;
|
||||
if (argc > 1) {
|
||||
if (JS_ToInt32(js, &hash_len, argv[1]))
|
||||
return JS_EXCEPTION;
|
||||
if (hash_len < 1 || hash_len > 64)
|
||||
return JS_RaiseDisrupt(js, "crypto.blake2: hash length must be between 1 and 64 bytes");
|
||||
}
|
||||
|
||||
uint8_t hash[64];
|
||||
// Use (bits + 7) / 8 to get byte length covering all bits
|
||||
crypto_blake2b(hash, hash_len, data, (data_bits + 7) / 8);
|
||||
|
||||
return js_new_blob_stoned_copy(js, hash, hash_len);
|
||||
}
|
||||
|
||||
JSValue js_crypto_sign(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
if (argc < 2) return JS_RaiseDisrupt(js, "crypto.sign: expected secret_key, message");
|
||||
|
||||
uint8_t *sk = get_blob_check_bits(js, argv[0], 512, "crypto.sign secret_key");
|
||||
if (!sk) return JS_EXCEPTION;
|
||||
|
||||
size_t msg_bits;
|
||||
uint8_t *msg = get_blob_any(js, argv[1], &msg_bits, "crypto.sign message");
|
||||
if (!msg) return JS_EXCEPTION;
|
||||
|
||||
uint8_t sig[64];
|
||||
crypto_eddsa_sign(sig, sk, msg, (msg_bits + 7) / 8);
|
||||
|
||||
return js_new_blob_stoned_copy(js, sig, 64);
|
||||
}
|
||||
|
||||
JSValue js_crypto_verify(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.verify: expected signature, public_key, message");
|
||||
|
||||
uint8_t *sig = get_blob_check_bits(js, argv[0], 512, "crypto.verify signature");
|
||||
if (!sig) return JS_EXCEPTION;
|
||||
|
||||
uint8_t *pk = get_blob_check_bits(js, argv[1], 256, "crypto.verify public_key");
|
||||
if (!pk) return JS_EXCEPTION;
|
||||
|
||||
size_t msg_bits;
|
||||
uint8_t *msg = get_blob_any(js, argv[2], &msg_bits, "crypto.verify message");
|
||||
if (!msg) return JS_EXCEPTION;
|
||||
|
||||
int ret = crypto_eddsa_check(sig, pk, msg, (msg_bits + 7) / 8);
|
||||
return JS_NewBool(js, ret == 0);
|
||||
}
|
||||
|
||||
JSValue js_crypto_lock(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.lock: expected key, nonce, message, [ad]");
|
||||
|
||||
uint8_t *key = get_blob_check_bits(js, argv[0], 256, "crypto.lock key");
|
||||
if (!key) return JS_EXCEPTION;
|
||||
|
||||
uint8_t *nonce = get_blob_check_bits(js, argv[1], 192, "crypto.lock nonce");
|
||||
if (!nonce) return JS_EXCEPTION;
|
||||
|
||||
size_t msg_bits;
|
||||
uint8_t *msg = get_blob_any(js, argv[2], &msg_bits, "crypto.lock message");
|
||||
if (!msg) return JS_EXCEPTION;
|
||||
size_t msg_len = (msg_bits + 7) / 8;
|
||||
|
||||
size_t ad_len = 0;
|
||||
uint8_t *ad = NULL;
|
||||
if (argc > 3 && !JS_IsNull(argv[3])) {
|
||||
size_t ad_bits;
|
||||
ad = get_blob_any(js, argv[3], &ad_bits, "crypto.lock ad");
|
||||
if (!ad) return JS_EXCEPTION;
|
||||
ad_len = (ad_bits + 7) / 8;
|
||||
}
|
||||
|
||||
size_t out_len = msg_len + 16;
|
||||
uint8_t *out = malloc(out_len);
|
||||
if (!out) return JS_RaiseOOM(js);
|
||||
|
||||
// Output: [Ciphertext (msg_len)] [MAC (16)]
|
||||
crypto_aead_lock(out, out + msg_len, key, nonce, ad, ad_len, msg, msg_len);
|
||||
|
||||
JSValue ret = js_new_blob_stoned_copy(js, out, out_len);
|
||||
free(out);
|
||||
return ret;
|
||||
}
|
||||
|
||||
JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.unlock: expected key, nonce, ciphertext, [ad]");
|
||||
|
||||
uint8_t *key = get_blob_check_bits(js, argv[0], 256, "crypto.unlock key");
|
||||
if (!key) return JS_EXCEPTION;
|
||||
|
||||
uint8_t *nonce = get_blob_check_bits(js, argv[1], 192, "crypto.unlock nonce");
|
||||
if (!nonce) return JS_EXCEPTION;
|
||||
|
||||
size_t cipher_bits;
|
||||
uint8_t *cipher = get_blob_any(js, argv[2], &cipher_bits, "crypto.unlock ciphertext");
|
||||
if (!cipher) return JS_EXCEPTION;
|
||||
|
||||
size_t cipher_len = (cipher_bits + 7) / 8;
|
||||
if (cipher_len < 16) return JS_RaiseDisrupt(js, "crypto.unlock: ciphertext too short (min 16 bytes)");
|
||||
|
||||
size_t msg_len = cipher_len - 16;
|
||||
|
||||
size_t ad_len = 0;
|
||||
uint8_t *ad = NULL;
|
||||
if (argc > 3 && !JS_IsNull(argv[3])) {
|
||||
size_t ad_bits;
|
||||
ad = get_blob_any(js, argv[3], &ad_bits, "crypto.unlock ad");
|
||||
if (!ad) return JS_EXCEPTION;
|
||||
ad_len = (ad_bits + 7) / 8;
|
||||
}
|
||||
|
||||
uint8_t *out = malloc(msg_len > 0 ? msg_len : 1);
|
||||
if (!out) return JS_RaiseOOM(js);
|
||||
|
||||
// MAC is at cipher + msg_len
|
||||
const uint8_t *mac = cipher + msg_len;
|
||||
|
||||
if (crypto_aead_unlock(out, mac, key, nonce, ad, ad_len, cipher, msg_len) != 0) {
|
||||
free(out);
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
JSValue ret = js_new_blob_stoned_copy(js, out, msg_len);
|
||||
free(out);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_crypto_funcs[] = {
|
||||
JS_CFUNC_DEF("shared", 2, js_crypto_shared),
|
||||
JS_CFUNC_DEF("blake2", 2, js_crypto_blake2),
|
||||
JS_CFUNC_DEF("sign", 2, js_crypto_sign),
|
||||
JS_CFUNC_DEF("verify", 3, js_crypto_verify),
|
||||
JS_CFUNC_DEF("lock", 3, js_crypto_lock),
|
||||
JS_CFUNC_DEF("unlock", 3, js_crypto_unlock),
|
||||
};
|
||||
|
||||
JSValue js_core_internal_crypto_use(JSContext *js)
|
||||
{
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, mod.val, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ function use_embed(name) {
|
||||
|
||||
var fd = use_embed('internal_fd')
|
||||
var js = use_embed('js')
|
||||
var crypto = use_embed('crypto')
|
||||
var crypto = use_embed('internal_crypto')
|
||||
|
||||
// core_path and shop_path come from env (C runtime passes them through)
|
||||
// shop_path may be null if --core was used without --shop
|
||||
@@ -58,7 +58,7 @@ function boot_load(name) {
|
||||
var mcode_blob = null
|
||||
var mach_blob = null
|
||||
if (!fd.is_file(mcode_path)) {
|
||||
print("error: missing boot seed: " + name + "\n")
|
||||
os.print("error: missing boot seed: " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
mcode_blob = fd.slurp(mcode_path)
|
||||
@@ -103,7 +103,7 @@ function load_pipeline_module(name, env) {
|
||||
tok_result = boot_tok(src, source_path)
|
||||
ast = boot_par(tok_result.tokens, src, source_path, boot_tok)
|
||||
if (ast.errors != null && length(ast.errors) > 0) {
|
||||
print("error: failed to compile pipeline module: " + name + "\n")
|
||||
os.print("error: failed to compile pipeline module: " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
ast = boot_fld(ast)
|
||||
@@ -126,7 +126,7 @@ function load_pipeline_module(name, env) {
|
||||
mach_blob = mach_compile_mcode_bin(name, text(mcode_blob))
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
print("error: cannot load pipeline module: " + name + "\n")
|
||||
os.print("error: cannot load pipeline module: " + name + "\n")
|
||||
disrupt
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ function analyze(src, filename) {
|
||||
var line = null
|
||||
var col = null
|
||||
var has_errors = _ast.errors != null && length(_ast.errors) > 0
|
||||
var folded = null
|
||||
if (has_errors) {
|
||||
while (_i < length(_ast.errors)) {
|
||||
e = _ast.errors[_i]
|
||||
@@ -166,9 +167,9 @@ function analyze(src, filename) {
|
||||
col = e.column
|
||||
if (msg != prev_msg || line != prev_line) {
|
||||
if (line != null && col != null)
|
||||
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
|
||||
os.print(`${filename}:${text(line)}:${text(col)}: error: ${msg}\n`)
|
||||
else
|
||||
print(`${filename}: error: ${msg}`)
|
||||
os.print(`${filename}: error: ${msg}\n`)
|
||||
}
|
||||
prev_line = line
|
||||
prev_msg = msg
|
||||
@@ -176,15 +177,126 @@ function analyze(src, filename) {
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
return fold_mod(_ast)
|
||||
folded = fold_mod(_ast)
|
||||
if (!_no_warn && folded._diagnostics != null && length(folded._diagnostics) > 0) {
|
||||
_i = 0
|
||||
while (_i < length(folded._diagnostics)) {
|
||||
e = folded._diagnostics[_i]
|
||||
os.print(`${filename}:${text(e.line)}:${text(e.col)}: ${e.severity}: ${e.message}\n`)
|
||||
_i = _i + 1
|
||||
}
|
||||
}
|
||||
folded._diagnostics = null
|
||||
return folded
|
||||
}
|
||||
|
||||
// Lazy-loaded verify_ir module (loaded on first use)
|
||||
var _verify_ir_mod = null
|
||||
|
||||
// Module summary extraction for cross-program analysis.
|
||||
// Scans mcode IR for use() call patterns and attaches summaries.
|
||||
// _summary_resolver is set after shop loads (null during bootstrap).
|
||||
var _summary_resolver = null
|
||||
|
||||
function extract_module_summaries(compiled, ctx) {
|
||||
if (_summary_resolver == null) return null
|
||||
var instrs = null
|
||||
var summaries = []
|
||||
var unresolved = []
|
||||
var i = 0
|
||||
var j = 0
|
||||
var n = 0
|
||||
var instr = null
|
||||
var prev = null
|
||||
var op = null
|
||||
var use_slots = {}
|
||||
var frame_map = {}
|
||||
var arg_map = {}
|
||||
var val_slot = 0
|
||||
var f_slot = 0
|
||||
var path = null
|
||||
var result_slot = 0
|
||||
var summary = null
|
||||
var inv_n = 0
|
||||
|
||||
if (compiled.main == null) return null
|
||||
instrs = compiled.main.instructions
|
||||
if (instrs == null) return null
|
||||
n = length(instrs)
|
||||
|
||||
// Pass 1: find access(slot, {make:"intrinsic", name:"use"})
|
||||
i = 0
|
||||
while (i < n) {
|
||||
instr = instrs[i]
|
||||
if (is_array(instr) && instr[0] == "access") {
|
||||
if (is_object(instr[2]) && instr[2].make == "intrinsic" && instr[2].name == "use") {
|
||||
use_slots[text(instr[1])] = true
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
// Pass 2: find frame(frame_slot, use_slot), setarg with string, invoke
|
||||
i = 0
|
||||
while (i < n) {
|
||||
instr = instrs[i]
|
||||
if (is_array(instr)) {
|
||||
op = instr[0]
|
||||
if (op == "frame" || op == "goframe") {
|
||||
if (use_slots[text(instr[2])] == true) {
|
||||
frame_map[text(instr[1])] = true
|
||||
}
|
||||
} else if (op == "setarg") {
|
||||
if (frame_map[text(instr[1])] == true) {
|
||||
val_slot = instr[3]
|
||||
j = i - 1
|
||||
while (j >= 0) {
|
||||
prev = instrs[j]
|
||||
if (is_array(prev) && prev[0] == "access" && prev[1] == val_slot && is_text(prev[2])) {
|
||||
arg_map[text(instr[1])] = prev[2]
|
||||
break
|
||||
}
|
||||
j = j - 1
|
||||
}
|
||||
}
|
||||
} else if (op == "invoke" || op == "tail_invoke") {
|
||||
f_slot = instr[1]
|
||||
path = arg_map[text(f_slot)]
|
||||
if (path != null) {
|
||||
result_slot = instr[2]
|
||||
summary = _summary_resolver(path, ctx)
|
||||
if (summary != null) {
|
||||
if (summary._native != true) {
|
||||
summaries[] = {slot: result_slot, summary: summary}
|
||||
}
|
||||
} else {
|
||||
inv_n = length(instr)
|
||||
unresolved[] = {path: path, line: instr[inv_n - 2], col: instr[inv_n - 1]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
if (length(summaries) > 0 || length(unresolved) > 0) {
|
||||
return {summaries: summaries, unresolved: unresolved}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Run AST through mcode pipeline -> register VM
|
||||
function run_ast_fn(name, ast, env) {
|
||||
function run_ast_fn(name, ast, env, pkg) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var ms = null
|
||||
var _ui = 0
|
||||
var _ur = null
|
||||
var optimized = null
|
||||
var _di = 0
|
||||
var _diag = null
|
||||
var _has_errors = false
|
||||
var mcode_json = null
|
||||
var mach_blob = null
|
||||
if (os._verify_ir) {
|
||||
if (_verify_ir_mod == null) {
|
||||
_verify_ir_mod = load_pipeline_module('verify_ir', pipeline_env)
|
||||
@@ -192,13 +304,45 @@ function run_ast_fn(name, ast, env) {
|
||||
compiled._verify = true
|
||||
compiled._verify_mod = _verify_ir_mod
|
||||
}
|
||||
var optimized = streamline_mod(compiled)
|
||||
if (!_no_warn) {
|
||||
compiled._warn = true
|
||||
ms = extract_module_summaries(compiled, pkg)
|
||||
if (ms != null) {
|
||||
if (length(ms.summaries) > 0) {
|
||||
compiled._module_summaries = ms.summaries
|
||||
}
|
||||
if (length(ms.unresolved) > 0) {
|
||||
compiled._unresolved_imports = ms.unresolved
|
||||
}
|
||||
}
|
||||
}
|
||||
if (compiled._unresolved_imports != null) {
|
||||
_ui = 0
|
||||
while (_ui < length(compiled._unresolved_imports)) {
|
||||
_ur = compiled._unresolved_imports[_ui]
|
||||
os.print(`${name}:${text(_ur.line)}:${text(_ur.col)}: error: cannot resolve module '${_ur.path}'\n`)
|
||||
_ui = _ui + 1
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
optimized = streamline_mod(compiled)
|
||||
if (optimized._verify) {
|
||||
delete optimized._verify
|
||||
delete optimized._verify_mod
|
||||
}
|
||||
var mcode_json = json.encode(optimized)
|
||||
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) {
|
||||
_di = 0
|
||||
_has_errors = false
|
||||
while (_di < length(optimized._diagnostics)) {
|
||||
_diag = optimized._diagnostics[_di]
|
||||
os.print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`)
|
||||
if (_diag.severity == "error") _has_errors = true
|
||||
_di = _di + 1
|
||||
}
|
||||
if (_has_errors) disrupt
|
||||
}
|
||||
mcode_json = json.encode(optimized)
|
||||
mach_blob = mach_compile_mcode_bin(name, mcode_json)
|
||||
return mach_load(mach_blob, env)
|
||||
}
|
||||
|
||||
@@ -217,6 +361,52 @@ function compile_to_blob(name, ast) {
|
||||
return mach_compile_mcode_bin(name, json.encode(optimized))
|
||||
}
|
||||
|
||||
// Compile user program AST to blob with diagnostics
|
||||
function compile_user_blob(name, ast, pkg) {
|
||||
var compiled = mcode_mod(ast)
|
||||
var ms = null
|
||||
var _ui = 0
|
||||
var _ur = null
|
||||
var optimized = null
|
||||
var _di = 0
|
||||
var _diag = null
|
||||
var _has_errors = false
|
||||
if (!_no_warn) {
|
||||
compiled._warn = true
|
||||
ms = extract_module_summaries(compiled, pkg)
|
||||
if (ms != null) {
|
||||
if (length(ms.summaries) > 0) {
|
||||
compiled._module_summaries = ms.summaries
|
||||
}
|
||||
if (length(ms.unresolved) > 0) {
|
||||
compiled._unresolved_imports = ms.unresolved
|
||||
}
|
||||
}
|
||||
}
|
||||
if (compiled._unresolved_imports != null) {
|
||||
_ui = 0
|
||||
while (_ui < length(compiled._unresolved_imports)) {
|
||||
_ur = compiled._unresolved_imports[_ui]
|
||||
os.print(`${name}:${text(_ur.line)}:${text(_ur.col)}: error: cannot resolve module '${_ur.path}'\n`)
|
||||
_ui = _ui + 1
|
||||
}
|
||||
disrupt
|
||||
}
|
||||
optimized = streamline_mod(compiled)
|
||||
if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) {
|
||||
_di = 0
|
||||
_has_errors = false
|
||||
while (_di < length(optimized._diagnostics)) {
|
||||
_diag = optimized._diagnostics[_di]
|
||||
os.print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`)
|
||||
if (_diag.severity == "error") _has_errors = true
|
||||
_di = _di + 1
|
||||
}
|
||||
if (_has_errors) disrupt
|
||||
}
|
||||
return mach_compile_mcode_bin(name, json.encode(optimized))
|
||||
}
|
||||
|
||||
// If loaded directly by C runtime (not via bootstrap), convert args -> init
|
||||
var _program = null
|
||||
var _user_args = []
|
||||
@@ -227,6 +417,9 @@ var _init = init
|
||||
if (_init != null && _init.native_mode)
|
||||
native_mode = true
|
||||
|
||||
// Inherit warn mode from init (set by C for --no-warn)
|
||||
var _no_warn = (_init != null && _init.no_warn) ? true : false
|
||||
|
||||
// CLI path: convert args to init record
|
||||
if (args != null && (_init == null || !_init.program)) {
|
||||
_program = args[0]
|
||||
@@ -242,7 +435,7 @@ if (args != null && (_init == null || !_init.program)) {
|
||||
}
|
||||
}
|
||||
|
||||
use_cache['core/os'] = os
|
||||
use_cache['core/internal/os'] = os
|
||||
|
||||
// Extra env properties added as engine initializes (log, runtime fns, etc.)
|
||||
var core_extras = {}
|
||||
@@ -291,7 +484,7 @@ function use_core(path) {
|
||||
result = mach_load(mach_blob, env)
|
||||
}
|
||||
} disruption {
|
||||
print("use('" + path + "'): failed to compile or load " + file_path + "\n")
|
||||
os.print("use('" + path + "'): failed to compile or load " + file_path + "\n")
|
||||
disrupt
|
||||
}
|
||||
_load_mod()
|
||||
@@ -314,8 +507,8 @@ function actor() {
|
||||
}
|
||||
|
||||
var actor_mod = use_core('actor')
|
||||
var wota = use_core('wota')
|
||||
var nota = use_core('nota')
|
||||
var wota = use_core('internal/wota')
|
||||
var nota = use_core('internal/nota')
|
||||
|
||||
|
||||
var ENETSERVICE = 0.1
|
||||
@@ -324,17 +517,37 @@ var REPLYTIMEOUT = 60 // seconds before replies are ignored
|
||||
// --- Logging system (bootstrap phase) ---
|
||||
// Early log: prints to console before toml/time/json are loaded.
|
||||
// Upgraded to full sink-based system after config loads (see load_log_config below).
|
||||
// The bootstrap log forwards to _log_full once the full system is ready, so that
|
||||
// modules loaded early (like shop.cm) get full logging even though they captured
|
||||
// the bootstrap function reference.
|
||||
|
||||
var log_config = null
|
||||
var channel_sinks = {}
|
||||
var wildcard_sinks = []
|
||||
var warned_channels = {}
|
||||
var stack_channels = {}
|
||||
var _log_full = null
|
||||
|
||||
var log_quiet_channels = { shop: true }
|
||||
|
||||
function log(name, args) {
|
||||
if (_log_full) return _log_full(name, args)
|
||||
if (log_quiet_channels[name]) return
|
||||
var msg = args[0]
|
||||
var stk = null
|
||||
var i = 0
|
||||
var fr = null
|
||||
if (msg == null) msg = ""
|
||||
os.print(`[${text(_cell.id, 0, 5)}] [${name}]: ${msg}\n`)
|
||||
if (name == "error") {
|
||||
stk = os.stack(2)
|
||||
if (stk && length(stk) > 0) {
|
||||
for (i = 0; i < length(stk); i = i + 1) {
|
||||
fr = stk[i]
|
||||
os.print(` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function actor_die(err)
|
||||
@@ -420,7 +633,23 @@ core_extras.native_mode = native_mode
|
||||
|
||||
// NOW load shop -- it receives all of the above via env
|
||||
var shop = use_core('internal/shop')
|
||||
if (native_mode) use_core('build')
|
||||
use_core('build')
|
||||
|
||||
// Wire up module summary resolver now that shop is available
|
||||
_summary_resolver = function(path, ctx) {
|
||||
var info = shop.resolve_import_info(path, ctx)
|
||||
if (info == null) return null
|
||||
if (info.type == 'native') return {_native: true}
|
||||
var resolved = info.resolved_path
|
||||
if (resolved == null) return null
|
||||
var summary_fn = function() {
|
||||
return shop.summary_file(resolved)
|
||||
} disruption {
|
||||
return null
|
||||
}
|
||||
return summary_fn()
|
||||
}
|
||||
|
||||
var time = use_core('time')
|
||||
var toml = use_core('toml')
|
||||
|
||||
@@ -447,6 +676,7 @@ function build_sink_routing() {
|
||||
var names = array(log_config.sink)
|
||||
arrfor(names, function(name) {
|
||||
var sink = log_config.sink[name]
|
||||
if (!sink || !is_object(sink)) return
|
||||
sink._name = name
|
||||
if (!is_array(sink.channels)) sink.channels = []
|
||||
if (is_text(sink.exclude)) sink.exclude = [sink.exclude]
|
||||
@@ -476,13 +706,13 @@ function load_log_config() {
|
||||
log_config = toml.decode(text(fd.slurp(log_path)))
|
||||
}
|
||||
}
|
||||
if (!log_config || !log_config.sink) {
|
||||
if (!log_config || !log_config.sink || length(array(log_config.sink)) == 0) {
|
||||
log_config = {
|
||||
sink: {
|
||||
terminal: {
|
||||
type: "console",
|
||||
format: "pretty",
|
||||
channels: ["console", "error", "system"],
|
||||
channels: ["*"],
|
||||
stack: ["error"]
|
||||
}
|
||||
}
|
||||
@@ -498,10 +728,8 @@ function pretty_format(rec) {
|
||||
var out = null
|
||||
var i = 0
|
||||
var fr = null
|
||||
if (rec.source && rec.source.file)
|
||||
src = rec.source.file + ":" + text(rec.source.line)
|
||||
ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
|
||||
out = `[${aid}] [${rec.channel}] ${src} ${ev}\n`
|
||||
out = `[${aid}] [${rec.channel}] ${ev}\n`
|
||||
if (rec.stack && length(rec.stack) > 0) {
|
||||
for (i = 0; i < length(rec.stack); i = i + 1) {
|
||||
fr = rec.stack[i]
|
||||
@@ -561,13 +789,7 @@ log = function(name, args) {
|
||||
var stack = null
|
||||
var rec = null
|
||||
|
||||
if (!sinks && length(wildcard_sinks) == 0) {
|
||||
if (!warned_channels[name]) {
|
||||
warned_channels[name] = true
|
||||
os.print(`[warn] log channel '${name}' has no sinks configured\n`)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!sinks && length(wildcard_sinks) == 0) return
|
||||
|
||||
// C-provided stack (from JS_Log callback) overrides caller_info/os.stack
|
||||
if (c_stack && length(c_stack) > 0) {
|
||||
@@ -594,6 +816,10 @@ log = function(name, args) {
|
||||
// Wire C-level JS_Log through the ƿit log system
|
||||
actor_mod.set_log(log)
|
||||
|
||||
// Let the bootstrap log forward to the full system — modules loaded early
|
||||
// (before the full log was ready) captured the bootstrap function reference.
|
||||
_log_full = log
|
||||
|
||||
var pronto = use_core('pronto')
|
||||
var fallback = pronto.fallback
|
||||
var parallel = pronto.parallel
|
||||
@@ -1209,27 +1435,38 @@ if (ends_with(prog, '.ce')) prog = text(prog, 0, -3)
|
||||
|
||||
var package = use_core('package')
|
||||
|
||||
// Find the .ce file
|
||||
var prog_path = prog + ".ce"
|
||||
var pkg_dir = null
|
||||
var core_dir = null
|
||||
if (!fd.is_file(prog_path)) {
|
||||
pkg_dir = package.find_package_dir(".")
|
||||
if (pkg_dir)
|
||||
prog_path = pkg_dir + '/' + prog + '.ce'
|
||||
}
|
||||
if (!fd.is_file(prog_path)) {
|
||||
// Check core packages
|
||||
core_dir = core_path
|
||||
prog_path = core_dir + '/' + prog + '.ce'
|
||||
}
|
||||
if (!fd.is_file(prog_path)) {
|
||||
os.print(`Main program ${prog} could not be found\n`)
|
||||
os.exit(1)
|
||||
// Find the .ce file using unified resolver
|
||||
var cwd_package = package.find_package_dir(".")
|
||||
var prog_info = shop.resolve_program ? shop.resolve_program(prog, cwd_package) : null
|
||||
var prog_path = null
|
||||
if (prog_info) {
|
||||
prog_path = prog_info.path
|
||||
} else {
|
||||
// Fallback: check CWD, package dir, and core
|
||||
prog_path = prog + ".ce"
|
||||
if (!fd.is_file(prog_path) && cwd_package)
|
||||
prog_path = cwd_package + '/' + prog + '.ce'
|
||||
if (!fd.is_file(prog_path))
|
||||
prog_path = core_path + '/' + prog + '.ce'
|
||||
if (!fd.is_file(prog_path)) {
|
||||
os.print(`Main program ${prog} could not be found\n`)
|
||||
os.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
$_.clock(_ => {
|
||||
var file_info = shop.file_info ? shop.file_info(prog_path) : null
|
||||
var _file_info_ok = false
|
||||
var file_info = null
|
||||
var _try_fi = function() {
|
||||
file_info = shop.file_info ? shop.file_info(prog_path) : null
|
||||
_file_info_ok = true
|
||||
} disruption {}
|
||||
_try_fi()
|
||||
if (!_file_info_ok || !file_info)
|
||||
file_info = {path: prog_path, is_module: false, is_actor: true, package: null, name: prog}
|
||||
// If the unified resolver found the package, use that as the authoritative source
|
||||
if (prog_info && prog_info.pkg)
|
||||
file_info.package = prog_info.pkg
|
||||
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
|
||||
|
||||
// Build env with runtime functions + capability injections
|
||||
@@ -1248,12 +1485,49 @@ $_.clock(_ => {
|
||||
}
|
||||
|
||||
var pkg = file_info ? file_info.package : null
|
||||
|
||||
// Verify all transitive dependency packages are present, auto-install if missing
|
||||
var _deps = null
|
||||
var _di = 0
|
||||
var _dep_dir = null
|
||||
var _auto_install = null
|
||||
if (pkg) {
|
||||
_deps = package.gather_dependencies(pkg)
|
||||
_di = 0
|
||||
while (_di < length(_deps)) {
|
||||
_dep_dir = package.get_dir(_deps[_di])
|
||||
if (!fd.is_dir(_dep_dir)) {
|
||||
log.console('installing missing dependency: ' + _deps[_di])
|
||||
_auto_install = function() {
|
||||
shop.sync(_deps[_di])
|
||||
} disruption {
|
||||
log.error('failed to install dependency: ' + _deps[_di])
|
||||
disrupt
|
||||
}
|
||||
_auto_install()
|
||||
}
|
||||
_di = _di + 1
|
||||
}
|
||||
}
|
||||
|
||||
env.use = function(path) {
|
||||
var ck = 'core/' + path
|
||||
var _use_core_result = null
|
||||
var _use_core_ok = false
|
||||
if (use_cache[ck]) return use_cache[ck]
|
||||
var core_mod = use_core(path)
|
||||
if (core_mod) return core_mod
|
||||
return shop.use(path, pkg)
|
||||
var _try_core = function() {
|
||||
_use_core_result = use_core(path)
|
||||
_use_core_ok = true
|
||||
} disruption {}
|
||||
_try_core()
|
||||
if (_use_core_ok && _use_core_result) return _use_core_result
|
||||
var _shop_use = function() {
|
||||
return shop.use(path, pkg)
|
||||
} disruption {
|
||||
log.error(`use('${path}') failed (package: ${pkg})`)
|
||||
disrupt
|
||||
}
|
||||
return _shop_use()
|
||||
}
|
||||
env.args = _cell.args.arg
|
||||
env.log = log
|
||||
@@ -1293,7 +1567,7 @@ $_.clock(_ => {
|
||||
} else {
|
||||
script = text(source_blob)
|
||||
ast = analyze(script, prog_path)
|
||||
mach_blob = compile_to_blob(prog, ast)
|
||||
mach_blob = compile_user_blob(prog, ast, pkg)
|
||||
if (cached_path) {
|
||||
ensure_build_dir()
|
||||
fd.slurpwrite(cached_path, mach_blob)
|
||||
|
||||
@@ -40,7 +40,8 @@ static int js2fd(JSContext *ctx, JSValueConst val)
|
||||
// Helper function for writing
|
||||
static ssize_t js_fd_write_helper(JSContext *js, int fd, JSValue val)
|
||||
{
|
||||
|
||||
(void)js; (void)fd; (void)val;
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +86,7 @@ JSC_CCALL(fd_write,
|
||||
JS_FreeCString(js, data);
|
||||
} else {
|
||||
void *data = js_get_blob_data(js, &len, argv[1]);
|
||||
if (data == -1)
|
||||
if (data == (void *)-1)
|
||||
return JS_EXCEPTION;
|
||||
wrote = write(fd, data, len);
|
||||
}
|
||||
@@ -101,18 +102,15 @@ JSC_CCALL(fd_read,
|
||||
if (argc > 1)
|
||||
size = js2number(js, argv[1]);
|
||||
|
||||
void *buf = malloc(size);
|
||||
if (!buf)
|
||||
return JS_RaiseDisrupt(js, "malloc failed");
|
||||
|
||||
ssize_t bytes_read = read(fd, buf, size);
|
||||
if (bytes_read < 0) {
|
||||
free(buf);
|
||||
void *out;
|
||||
ret = js_new_blob_alloc(js, size, &out);
|
||||
if (JS_IsException(ret)) return ret;
|
||||
|
||||
ssize_t bytes_read = read(fd, out, size);
|
||||
if (bytes_read < 0)
|
||||
return JS_RaiseDisrupt(js, "read failed: %s", strerror(errno));
|
||||
}
|
||||
|
||||
ret = js_new_blob_stoned_copy(js, buf, bytes_read);
|
||||
free(buf);
|
||||
|
||||
js_blob_stone(ret, bytes_read);
|
||||
return ret;
|
||||
)
|
||||
|
||||
@@ -127,39 +125,48 @@ JSC_SCALL(fd_slurp,
|
||||
size_t size = st.st_size;
|
||||
if (size == 0)
|
||||
return js_new_blob_stoned_copy(js, NULL, 0);
|
||||
|
||||
|
||||
#ifndef _WIN32
|
||||
int fd = open(str, O_RDONLY);
|
||||
if (fd < 0)
|
||||
return JS_RaiseDisrupt(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_RaiseDisrupt(js, "mmap failed: %s", strerror(errno));
|
||||
|
||||
void *out;
|
||||
ret = js_new_blob_alloc(js, size, &out);
|
||||
if (JS_IsException(ret)) { close(fd); return ret; }
|
||||
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
ssize_t n = read(fd, (uint8_t *)out + total, size - total);
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
close(fd);
|
||||
return JS_RaiseDisrupt(js, "read failed: %s", strerror(errno));
|
||||
}
|
||||
if (n == 0) break;
|
||||
total += n;
|
||||
}
|
||||
ret = js_new_blob_stoned_copy(js, data, size);
|
||||
munmap(data, size);
|
||||
close(fd);
|
||||
js_blob_stone(ret, total);
|
||||
#else
|
||||
// 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_RaiseDisrupt(js, "CreateFile failed: %lu", GetLastError());
|
||||
|
||||
|
||||
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
|
||||
if (hMapping == NULL) {
|
||||
CloseHandle(hFile);
|
||||
return JS_RaiseDisrupt(js, "CreateFileMapping failed: %lu", GetLastError());
|
||||
}
|
||||
|
||||
|
||||
void *data = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
|
||||
if (data == NULL) {
|
||||
CloseHandle(hMapping);
|
||||
CloseHandle(hFile);
|
||||
return JS_RaiseDisrupt(js, "MapViewOfFile failed: %lu", GetLastError());
|
||||
}
|
||||
|
||||
|
||||
ret = js_new_blob_stoned_copy(js, data, size);
|
||||
UnmapViewOfFile(data);
|
||||
CloseHandle(hMapping);
|
||||
|
||||
515
internal/fd_playdate.c
Normal file
515
internal/fd_playdate.c
Normal file
@@ -0,0 +1,515 @@
|
||||
#include "cell.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "pd_api.h"
|
||||
|
||||
// Global Playdate API pointers - defined in main_playdate.c
|
||||
extern const struct playdate_file *pd_file;
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
// Convert JS value to SDFile* (casting via intptr_t/int)
|
||||
static SDFile* js2fd(JSContext *ctx, JSValueConst val)
|
||||
{
|
||||
int32_t fd_int;
|
||||
if (JS_ToInt32(ctx, &fd_int, val) < 0) {
|
||||
JS_RaiseDisrupt(ctx, "Expected file descriptor number");
|
||||
return NULL;
|
||||
}
|
||||
return (SDFile*)(intptr_t)fd_int;
|
||||
}
|
||||
|
||||
static time_t pd_time_to_unix(FileStat *fs) {
|
||||
struct tm t = {0};
|
||||
t.tm_year = fs->m_year - 1900;
|
||||
t.tm_mon = fs->m_month - 1;
|
||||
t.tm_mday = fs->m_day;
|
||||
t.tm_hour = fs->m_hour;
|
||||
t.tm_min = fs->m_minute;
|
||||
t.tm_sec = fs->m_second;
|
||||
return mktime(&t);
|
||||
}
|
||||
|
||||
// --- Implementation ---
|
||||
|
||||
JSC_SCALL(fd_open,
|
||||
FileOptions flags = kFileRead;
|
||||
|
||||
// Parse optional flags argument
|
||||
if (argc > 1 && JS_IsText(argv[1])) {
|
||||
const char *flag_str = JS_ToCString(js, argv[1]);
|
||||
flags = 0;
|
||||
|
||||
if (strchr(flag_str, 'r')) flags |= kFileRead;
|
||||
if (strchr(flag_str, 'w')) flags |= kFileWrite;
|
||||
if (strchr(flag_str, 'a')) flags |= kFileAppend;
|
||||
if (strchr(flag_str, '+')) {
|
||||
// Read+Write not directly mapped, assume Write covers it or combine?
|
||||
// Playdate docs say kFileRead|kFileWrite is valid
|
||||
flags |= kFileRead | kFileWrite;
|
||||
}
|
||||
|
||||
JS_FreeCString(js, flag_str);
|
||||
}
|
||||
|
||||
SDFile* file = pd_file->open(str, flags);
|
||||
if (!file) {
|
||||
const char* err = pd_file->geterr();
|
||||
ret = JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error");
|
||||
} else {
|
||||
ret = JS_NewInt32(js, (int32_t)(intptr_t)file);
|
||||
}
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_write,
|
||||
SDFile* fd = js2fd(js, argv[0]);
|
||||
if (!fd) return JS_EXCEPTION;
|
||||
|
||||
size_t len;
|
||||
int wrote;
|
||||
if (JS_IsText(argv[1])) {
|
||||
const char *data = JS_ToCStringLen(js, &len, argv[1]);
|
||||
if (!data) return JS_EXCEPTION;
|
||||
wrote = pd_file->write(fd, data, (unsigned int)len);
|
||||
JS_FreeCString(js, data);
|
||||
} else {
|
||||
void *data = js_get_blob_data(js, &len, argv[1]);
|
||||
if (data == (void*)-1)
|
||||
return JS_EXCEPTION;
|
||||
wrote = pd_file->write(fd, data, (unsigned int)len);
|
||||
}
|
||||
|
||||
if (wrote < 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "write failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
return JS_NewInt64(js, wrote);
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_read,
|
||||
SDFile* fd = js2fd(js, argv[0]);
|
||||
if (!fd) return JS_EXCEPTION;
|
||||
|
||||
size_t size = 4096;
|
||||
if (argc > 1)
|
||||
size = js2number(js, argv[1]);
|
||||
|
||||
void *out;
|
||||
ret = js_new_blob_alloc(js, size, &out);
|
||||
if (JS_IsException(ret)) return ret;
|
||||
|
||||
int bytes_read = pd_file->read(fd, out, (unsigned int)size);
|
||||
if (bytes_read < 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "read failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
js_blob_stone(ret, bytes_read);
|
||||
return ret;
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_slurp,
|
||||
FileStat st;
|
||||
if (pd_file->stat(str, &st) != 0) {
|
||||
// const char* err = pd_file->geterr(); // stat usually returns -1 on error
|
||||
return JS_RaiseDisrupt(js, "stat failed for %s", str);
|
||||
}
|
||||
|
||||
if (st.isdir)
|
||||
return JS_RaiseDisrupt(js, "path is a directory");
|
||||
|
||||
size_t size = st.size;
|
||||
if (size == 0)
|
||||
return js_new_blob_stoned_copy(js, NULL, 0);
|
||||
|
||||
SDFile* fd = pd_file->open(str, kFileRead);
|
||||
if (!fd) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
void *out;
|
||||
ret = js_new_blob_alloc(js, size, &out);
|
||||
if (JS_IsException(ret)) { pd_file->close(fd); return ret; }
|
||||
|
||||
int bytes_read = pd_file->read(fd, out, (unsigned int)size);
|
||||
pd_file->close(fd);
|
||||
|
||||
if (bytes_read < 0)
|
||||
return JS_RaiseDisrupt(js, "read failed");
|
||||
|
||||
js_blob_stone(ret, bytes_read);
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_lseek,
|
||||
SDFile* fd = js2fd(js, argv[0]);
|
||||
if (!fd) return JS_EXCEPTION;
|
||||
|
||||
int offset = (int)js2number(js, argv[1]);
|
||||
int whence = SEEK_SET;
|
||||
|
||||
if (argc > 2) {
|
||||
const char *whence_str = JS_ToCString(js, argv[2]);
|
||||
if (strcmp(whence_str, "cur") == 0) whence = SEEK_CUR;
|
||||
else if (strcmp(whence_str, "end") == 0) whence = SEEK_END;
|
||||
JS_FreeCString(js, whence_str);
|
||||
}
|
||||
|
||||
if (pd_file->seek(fd, offset, whence) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "seek failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
// tell to get new pos
|
||||
int new_pos = pd_file->tell(fd);
|
||||
return JS_NewInt64(js, new_pos);
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_getcwd,
|
||||
// Playdate is always at root of its sandbox
|
||||
return JS_NewString(js, "/");
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_rmdir,
|
||||
int recursive = 0;
|
||||
if (argc > 1)
|
||||
recursive = JS_ToBool(js, argv[1]);
|
||||
|
||||
if (pd_file->unlink(str, recursive) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
ret = JS_RaiseDisrupt(js, "could not remove %s: %s", str, err ? err : "unknown error");
|
||||
}
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_mkdir,
|
||||
if (pd_file->mkdir(str) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
ret = JS_RaiseDisrupt(js, "could not make directory %s: %s", str, err ? err : "unknown error");
|
||||
}
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_mv,
|
||||
if (argc < 2)
|
||||
ret = JS_RaiseDisrupt(js, "fd.mv requires 2 arguments: old path and new path");
|
||||
else if (!JS_IsText(argv[1]))
|
||||
ret = JS_RaiseDisrupt(js, "second argument must be a string (new path)");
|
||||
else {
|
||||
const char *new_path = JS_ToCString(js, argv[1]);
|
||||
if (pd_file->rename(str, new_path) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
ret = JS_RaiseDisrupt(js, "could not rename %s to %s: %s", str, new_path, err ? err : "unknown error");
|
||||
}
|
||||
JS_FreeCString(js, new_path);
|
||||
}
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_symlink,
|
||||
// Not supported
|
||||
if (argc >= 2 && JS_IsText(argv[1])) {
|
||||
// consume arg
|
||||
JS_FreeCString(js, JS_ToCString(js, argv[1]));
|
||||
}
|
||||
ret = JS_RaiseDisrupt(js, "symlink not supported on Playdate");
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_unlink,
|
||||
if (pd_file->unlink(str, 0) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
ret = JS_RaiseDisrupt(js, "could not remove file %s: %s", str, err ? err : "unknown error");
|
||||
}
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_rm,
|
||||
// Recursive remove
|
||||
if (pd_file->unlink(str, 1) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
ret = JS_RaiseDisrupt(js, "could not remove %s: %s", str, err ? err : "unknown error");
|
||||
}
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_fsync,
|
||||
SDFile* fd = js2fd(js, argv[0]);
|
||||
if (!fd) return JS_EXCEPTION;
|
||||
|
||||
if (pd_file->flush(fd) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "fsync failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_close,
|
||||
SDFile* fd = js2fd(js, argv[0]);
|
||||
if (!fd) return JS_EXCEPTION;
|
||||
|
||||
if (pd_file->close(fd) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "close failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_fstat,
|
||||
// fstat not supported by Playdate API
|
||||
SDFile* fd = js2fd(js, argv[0]);
|
||||
if (!fd) return JS_EXCEPTION;
|
||||
|
||||
// We can't get stat from SDFile*. Return minimal object or error?
|
||||
// fd.c returns full object.
|
||||
// Return empty object with error property? Or just size 0?
|
||||
// Let's return empty object.
|
||||
JSValue obj = JS_NewObject(js);
|
||||
return obj;
|
||||
)
|
||||
|
||||
JSC_SCALL(fd_stat,
|
||||
FileStat st;
|
||||
if (pd_file->stat(str, &st) != 0) {
|
||||
return JS_NewObject(js); // Return empty on failure/not found
|
||||
}
|
||||
|
||||
JSValue obj = JS_NewObject(js);
|
||||
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.size));
|
||||
// Approximate mode
|
||||
JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.isdir ? 0040755 : 0100644));
|
||||
JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, 0));
|
||||
|
||||
time_t ts = pd_time_to_unix(&st);
|
||||
JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, ts));
|
||||
JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, ts));
|
||||
JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, ts));
|
||||
|
||||
JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, 1));
|
||||
JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 512));
|
||||
JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, (st.size + 511) / 512));
|
||||
|
||||
JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, !st.isdir));
|
||||
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, st.isdir));
|
||||
JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, 0));
|
||||
JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, 0));
|
||||
|
||||
return obj;
|
||||
)
|
||||
|
||||
struct listfiles_ctx {
|
||||
JSContext *js;
|
||||
JSValue array;
|
||||
int index;
|
||||
};
|
||||
|
||||
static void listfiles_cb(const char *path, void *userdata) {
|
||||
struct listfiles_ctx *ctx = (struct listfiles_ctx*)userdata;
|
||||
if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) return;
|
||||
|
||||
// Playdate listfiles returns just the name, but sometimes with slash for dir?
|
||||
// Docs say "names of files".
|
||||
|
||||
JS_SetPropertyNumber(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
|
||||
}
|
||||
|
||||
JSC_SCALL(fd_readdir,
|
||||
JSValue ret_arr = JS_NewArray(js);
|
||||
struct listfiles_ctx ctx = { js, ret_arr, 0 };
|
||||
|
||||
if (pd_file->listfiles(str, listfiles_cb, &ctx, 0) != 0) {
|
||||
const char* err = pd_file->geterr();
|
||||
JS_FreeValue(js, ret_arr);
|
||||
return JS_RaiseDisrupt(js, "listfiles failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
ret = ret_arr;
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_is_file,
|
||||
const char *path = JS_ToCString(js, argv[0]);
|
||||
if (!path) return JS_EXCEPTION;
|
||||
|
||||
FileStat st;
|
||||
int res = pd_file->stat(path, &st);
|
||||
JS_FreeCString(js, path);
|
||||
|
||||
if (res != 0) return JS_NewBool(js, 0);
|
||||
return JS_NewBool(js, !st.isdir);
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_is_dir,
|
||||
const char *path = JS_ToCString(js, argv[0]);
|
||||
if (!path) return JS_EXCEPTION;
|
||||
|
||||
FileStat st;
|
||||
int res = pd_file->stat(path, &st);
|
||||
JS_FreeCString(js, path);
|
||||
|
||||
if (res != 0) return JS_NewBool(js, 0);
|
||||
return JS_NewBool(js, st.isdir);
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_slurpwrite,
|
||||
size_t len;
|
||||
const char *data = js_get_blob_data(js, &len, argv[1]);
|
||||
|
||||
if (data == (const char *)-1)
|
||||
return JS_EXCEPTION;
|
||||
|
||||
const char *str = JS_ToCString(js, argv[0]);
|
||||
if (!str) return JS_EXCEPTION;
|
||||
|
||||
SDFile* fd = pd_file->open(str, kFileWrite | kFileRead); // Truncates by default? Playdate docs: kFileWrite truncates.
|
||||
JS_FreeCString(js, str);
|
||||
|
||||
if (!fd) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
int written = pd_file->write(fd, data, (unsigned int)len);
|
||||
pd_file->close(fd);
|
||||
|
||||
if (written != (int)len) {
|
||||
const char* err = pd_file->geterr();
|
||||
return JS_RaiseDisrupt(js, "write failed: %s", err ? err : "unknown error");
|
||||
}
|
||||
|
||||
return JS_NULL;
|
||||
)
|
||||
|
||||
// Helper for recursive enumeration
|
||||
static void visit_directory_pd(JSContext *js, JSValue results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
|
||||
// We can't use listfiles with arbitrary userdata easily if we need to pass multiple args without struct
|
||||
// We'll reuse listfiles but we need a way to pass context.
|
||||
// The context struct can hold current path info.
|
||||
|
||||
// Actually, listfiles takes a callback.
|
||||
// We need to implement recursion.
|
||||
// But listfiles doesn't give us full paths in callback, just names.
|
||||
// So we can reconstruct.
|
||||
|
||||
// We need a nested struct or helper.
|
||||
}
|
||||
|
||||
struct enum_ctx {
|
||||
JSContext *js;
|
||||
JSValue results;
|
||||
int *count;
|
||||
const char *curr_path;
|
||||
const char *rel_prefix;
|
||||
int recurse;
|
||||
};
|
||||
|
||||
static void enum_cb(const char *name, void *userdata) {
|
||||
struct enum_ctx *ctx = (struct enum_ctx*)userdata;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) return;
|
||||
|
||||
char item_rel[512]; // PATH_MAX
|
||||
if (ctx->rel_prefix && strlen(ctx->rel_prefix) > 0) {
|
||||
snprintf(item_rel, sizeof(item_rel), "%s/%s", ctx->rel_prefix, name);
|
||||
} else {
|
||||
strcpy(item_rel, name);
|
||||
}
|
||||
|
||||
JS_SetPropertyNumber(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel));
|
||||
|
||||
if (ctx->recurse) {
|
||||
// Check if directory
|
||||
char child_path[512];
|
||||
snprintf(child_path, sizeof(child_path), "%s/%s", ctx->curr_path, name);
|
||||
|
||||
// Remove trailing slash if name has it? Playdate names sometimes have trailing slash for dirs
|
||||
// But stat should handle it.
|
||||
|
||||
FileStat st;
|
||||
if (pd_file->stat(child_path, &st) == 0 && st.isdir) {
|
||||
struct enum_ctx subctx = *ctx;
|
||||
subctx.curr_path = child_path;
|
||||
subctx.rel_prefix = item_rel;
|
||||
pd_file->listfiles(child_path, enum_cb, &subctx, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSC_SCALL(fd_enumerate,
|
||||
const char *path = str;
|
||||
if (!path) path = ".";
|
||||
int recurse = 0;
|
||||
|
||||
if (argc > 1)
|
||||
recurse = JS_ToBool(js, argv[1]);
|
||||
|
||||
JSValue results = JS_NewArray(js);
|
||||
int result_count = 0;
|
||||
|
||||
FileStat st;
|
||||
if (pd_file->stat(path, &st) == 0 && st.isdir) {
|
||||
struct enum_ctx ctx = { js, results, &result_count, path, "", recurse };
|
||||
pd_file->listfiles(path, enum_cb, &ctx, 0);
|
||||
}
|
||||
|
||||
ret = results;
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_realpath,
|
||||
const char *path = JS_ToCString(js, argv[0]);
|
||||
if (!path) return JS_EXCEPTION;
|
||||
// Playdate has no realpath, just return path
|
||||
JSValue res = JS_NewString(js, path);
|
||||
JS_FreeCString(js, path);
|
||||
return res;
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_is_link,
|
||||
const char *path = JS_ToCString(js, argv[0]);
|
||||
JS_FreeCString(js, path);
|
||||
return JS_NewBool(js, 0);
|
||||
)
|
||||
|
||||
JSC_CCALL(fd_readlink,
|
||||
const char *path = JS_ToCString(js, argv[0]);
|
||||
JS_FreeCString(js, path);
|
||||
return JS_RaiseDisrupt(js, "readlink not supported");
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_fd_funcs[] = {
|
||||
MIST_FUNC_DEF(fd, open, 2),
|
||||
MIST_FUNC_DEF(fd, write, 2),
|
||||
MIST_FUNC_DEF(fd, read, 2),
|
||||
MIST_FUNC_DEF(fd, slurp, 1),
|
||||
MIST_FUNC_DEF(fd, slurpwrite, 2),
|
||||
MIST_FUNC_DEF(fd, lseek, 3),
|
||||
MIST_FUNC_DEF(fd, getcwd, 0),
|
||||
MIST_FUNC_DEF(fd, rmdir, 2),
|
||||
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),
|
||||
MIST_FUNC_DEF(fd, fstat, 1),
|
||||
MIST_FUNC_DEF(fd, readdir, 1),
|
||||
MIST_FUNC_DEF(fd, is_file, 1),
|
||||
MIST_FUNC_DEF(fd, is_dir, 1),
|
||||
MIST_FUNC_DEF(fd, is_link, 1),
|
||||
MIST_FUNC_DEF(fd, enumerate, 2),
|
||||
MIST_FUNC_DEF(fd, symlink, 2),
|
||||
MIST_FUNC_DEF(fd, realpath, 1),
|
||||
MIST_FUNC_DEF(fd, readlink, 1),
|
||||
};
|
||||
|
||||
JSValue js_fd_use(JSContext *js) {
|
||||
JSValue mod = JS_NewObject(js);
|
||||
JS_SetPropertyFunctionList(js, mod, js_fd_funcs, countof(js_fd_funcs));
|
||||
return mod;
|
||||
}
|
||||
@@ -8,26 +8,25 @@
|
||||
JSC_CCALL(kim_encode,
|
||||
const char *utf8_str = JS_ToCString(js, argv[0]);
|
||||
if (!utf8_str) return JS_EXCEPTION;
|
||||
|
||||
|
||||
// Count runes to estimate kim buffer size
|
||||
int rune_count = utf8_count(utf8_str);
|
||||
|
||||
// Allocate kim buffer (worst case: 5 bytes per rune)
|
||||
|
||||
// Allocate blob (worst case: 5 bytes per rune)
|
||||
size_t kim_size = rune_count * 5;
|
||||
char *kim_buffer = malloc(kim_size);
|
||||
char *kim_ptr = kim_buffer;
|
||||
|
||||
// Encode utf8 to kim
|
||||
void *out;
|
||||
ret = js_new_blob_alloc(js, kim_size, &out);
|
||||
if (JS_IsException(ret)) { JS_FreeCString(js, utf8_str); return ret; }
|
||||
|
||||
// Encode utf8 to kim directly into blob
|
||||
char *kim_ptr = (char *)out;
|
||||
long long runes_encoded;
|
||||
utf8_to_kim(&utf8_str, &kim_ptr, &runes_encoded);
|
||||
|
||||
// Calculate actual size used
|
||||
size_t actual_size = kim_ptr - kim_buffer;
|
||||
|
||||
// Create blob with the encoded data
|
||||
ret = js_new_blob_stoned_copy(js, kim_buffer, actual_size);
|
||||
|
||||
free(kim_buffer);
|
||||
|
||||
// Stone with actual size used
|
||||
size_t actual_size = kim_ptr - (char *)out;
|
||||
js_blob_stone(ret, actual_size);
|
||||
|
||||
JS_FreeCString(js, utf8_str);
|
||||
)
|
||||
|
||||
@@ -73,7 +72,7 @@ static const JSCFunctionListEntry js_kim_funcs[] = {
|
||||
MIST_FUNC_DEF(kim, decode, 1),
|
||||
};
|
||||
|
||||
JSValue js_core_kim_use(JSContext *js)
|
||||
JSValue js_core_internal_kim_use(JSContext *js)
|
||||
{
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
|
||||
431
internal/nota.c
Normal file
431
internal/nota.c
Normal file
@@ -0,0 +1,431 @@
|
||||
#define NOTA_IMPLEMENTATION
|
||||
#include "quickjs-internal.h"
|
||||
#include "cell.h"
|
||||
|
||||
static int nota_get_arr_len (JSContext *ctx, JSValue arr) {
|
||||
int64_t len;
|
||||
JS_GetLength (ctx, arr, &len);
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
typedef struct NotaVisitedNode {
|
||||
JSGCRef ref;
|
||||
struct NotaVisitedNode *next;
|
||||
} NotaVisitedNode;
|
||||
|
||||
typedef struct NotaEncodeContext {
|
||||
JSContext *ctx;
|
||||
NotaVisitedNode *visited_list;
|
||||
NotaBuffer nb;
|
||||
int cycle;
|
||||
JSGCRef *replacer_ref; /* pointer to GC-rooted ref */
|
||||
} NotaEncodeContext;
|
||||
|
||||
static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
|
||||
NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode));
|
||||
JS_PushGCRef (enc->ctx, &node->ref);
|
||||
node->ref.val = JS_DupValue (enc->ctx, val);
|
||||
node->next = enc->visited_list;
|
||||
enc->visited_list = node;
|
||||
}
|
||||
|
||||
static void nota_stack_pop (NotaEncodeContext *enc) {
|
||||
NotaVisitedNode *node = enc->visited_list;
|
||||
enc->visited_list = node->next;
|
||||
JS_FreeValue (enc->ctx, node->ref.val);
|
||||
JS_PopGCRef (enc->ctx, &node->ref);
|
||||
sys_free (node);
|
||||
}
|
||||
|
||||
static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) {
|
||||
NotaVisitedNode *node = enc->visited_list;
|
||||
while (node) {
|
||||
if (JS_StrictEq (enc->ctx, node->ref.val, val))
|
||||
return 1;
|
||||
node = node->next;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
|
||||
if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val);
|
||||
|
||||
JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) };
|
||||
JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, holder, 2, args);
|
||||
JS_FreeValue (enc->ctx, args[0]);
|
||||
JS_FreeValue (enc->ctx, args[1]);
|
||||
|
||||
if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char *js_do_nota_decode (JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
|
||||
int type = nota_type (nota);
|
||||
JSValue ret2;
|
||||
long long n;
|
||||
double d;
|
||||
int b;
|
||||
char *str;
|
||||
uint8_t *blob;
|
||||
|
||||
switch (type) {
|
||||
case NOTA_BLOB:
|
||||
nota = nota_read_blob (&n, (char **)&blob, nota);
|
||||
*tmp = js_new_blob_stoned_copy (js, blob, n);
|
||||
sys_free (blob);
|
||||
break;
|
||||
case NOTA_TEXT:
|
||||
nota = nota_read_text (&str, nota);
|
||||
*tmp = JS_NewString (js, str);
|
||||
sys_free (str);
|
||||
break;
|
||||
case NOTA_ARR:
|
||||
nota = nota_read_array (&n, nota);
|
||||
*tmp = JS_NewArrayLen (js, n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
nota = js_do_nota_decode (js, &ret2, nota, *tmp, JS_NewInt32 (js, i), reviver);
|
||||
JS_SetPropertyNumber (js, *tmp, i, ret2);
|
||||
}
|
||||
break;
|
||||
case NOTA_REC:
|
||||
nota = nota_read_record (&n, nota);
|
||||
*tmp = JS_NewObject (js);
|
||||
for (int i = 0; i < n; i++) {
|
||||
JSGCRef prop_key_ref, sub_val_ref;
|
||||
JS_PushGCRef (js, &prop_key_ref);
|
||||
JS_PushGCRef (js, &sub_val_ref);
|
||||
nota = nota_read_text (&str, nota);
|
||||
prop_key_ref.val = JS_NewString (js, str);
|
||||
sub_val_ref.val = JS_NULL;
|
||||
nota = js_do_nota_decode (js, &sub_val_ref.val, nota, *tmp, prop_key_ref.val, reviver);
|
||||
JS_SetPropertyStr (js, *tmp, str, sub_val_ref.val);
|
||||
JS_PopGCRef (js, &sub_val_ref);
|
||||
JS_PopGCRef (js, &prop_key_ref);
|
||||
sys_free (str);
|
||||
}
|
||||
break;
|
||||
case NOTA_INT:
|
||||
nota = nota_read_int (&n, nota);
|
||||
*tmp = JS_NewInt64 (js, n);
|
||||
break;
|
||||
case NOTA_SYM:
|
||||
nota = nota_read_sym (&b, nota);
|
||||
if (b == NOTA_PRIVATE) {
|
||||
JSGCRef inner_ref, obj_ref2;
|
||||
JS_PushGCRef (js, &inner_ref);
|
||||
JS_PushGCRef (js, &obj_ref2);
|
||||
inner_ref.val = JS_NULL;
|
||||
nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver);
|
||||
obj_ref2.val = JS_NewObject (js);
|
||||
if (!JS_IsNull (js->actor_sym))
|
||||
JS_SetPropertyKey (js, obj_ref2.val, js->actor_sym, inner_ref.val);
|
||||
JS_CellStone (js, obj_ref2.val);
|
||||
*tmp = obj_ref2.val;
|
||||
JS_PopGCRef (js, &obj_ref2);
|
||||
JS_PopGCRef (js, &inner_ref);
|
||||
} else {
|
||||
switch (b) {
|
||||
case NOTA_NULL: *tmp = JS_NULL; break;
|
||||
case NOTA_FALSE: *tmp = JS_NewBool (js, 0); break;
|
||||
case NOTA_TRUE: *tmp = JS_NewBool (js, 1); break;
|
||||
default: *tmp = JS_NULL; break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
case NOTA_FLOAT:
|
||||
nota = nota_read_float (&d, nota);
|
||||
*tmp = JS_NewFloat64 (js, d);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!JS_IsNull (reviver)) {
|
||||
JSValue args[2] = { JS_DupValue (js, key), JS_DupValue (js, *tmp) };
|
||||
JSValue revived = JS_Call (js, reviver, holder, 2, args);
|
||||
JS_FreeValue (js, args[0]);
|
||||
JS_FreeValue (js, args[1]);
|
||||
if (!JS_IsException (revived)) {
|
||||
JS_FreeValue (js, *tmp);
|
||||
*tmp = revived;
|
||||
} else {
|
||||
JS_FreeValue (js, revived);
|
||||
}
|
||||
}
|
||||
|
||||
return nota;
|
||||
}
|
||||
|
||||
static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
|
||||
JSContext *ctx = enc->ctx;
|
||||
JSGCRef replaced_ref, keys_ref, elem_ref, prop_ref;
|
||||
JS_PushGCRef (ctx, &replaced_ref);
|
||||
replaced_ref.val = nota_apply_replacer (enc, holder, key, val);
|
||||
int tag = JS_VALUE_GET_TAG (replaced_ref.val);
|
||||
|
||||
switch (tag) {
|
||||
case JS_TAG_INT:
|
||||
case JS_TAG_FLOAT64: {
|
||||
double d;
|
||||
JS_ToFloat64 (ctx, &d, replaced_ref.val);
|
||||
nota_write_number (&enc->nb, d);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_STRING: {
|
||||
const char *str = JS_ToCString (ctx, replaced_ref.val);
|
||||
nota_write_text (&enc->nb, str);
|
||||
JS_FreeCString (ctx, str);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_BOOL:
|
||||
if (JS_VALUE_GET_BOOL (replaced_ref.val)) nota_write_sym (&enc->nb, NOTA_TRUE);
|
||||
else nota_write_sym (&enc->nb, NOTA_FALSE);
|
||||
break;
|
||||
case JS_TAG_NULL:
|
||||
nota_write_sym (&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
case JS_TAG_PTR: {
|
||||
if (JS_IsText (replaced_ref.val)) {
|
||||
const char *str = JS_ToCString (ctx, replaced_ref.val);
|
||||
nota_write_text (&enc->nb, str);
|
||||
JS_FreeCString (ctx, str);
|
||||
break;
|
||||
}
|
||||
|
||||
if (js_is_blob (ctx, replaced_ref.val)) {
|
||||
size_t buf_len;
|
||||
void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val);
|
||||
if (buf_data == (void *)-1) {
|
||||
JS_PopGCRef (ctx, &replaced_ref);
|
||||
return;
|
||||
}
|
||||
nota_write_blob (&enc->nb, (unsigned long long)buf_len * 8, (const char *)buf_data);
|
||||
break;
|
||||
}
|
||||
|
||||
if (JS_IsArray (replaced_ref.val)) {
|
||||
if (nota_stack_has (enc, replaced_ref.val)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
nota_stack_push (enc, replaced_ref.val);
|
||||
int arr_len = nota_get_arr_len (ctx, replaced_ref.val);
|
||||
nota_write_array (&enc->nb, arr_len);
|
||||
JS_PushGCRef (ctx, &elem_ref);
|
||||
for (int i = 0; i < arr_len; i++) {
|
||||
elem_ref.val = JS_GetPropertyNumber (ctx, replaced_ref.val, i);
|
||||
JSValue elem_key = JS_NewInt32 (ctx, i);
|
||||
nota_encode_value (enc, elem_ref.val, replaced_ref.val, elem_key);
|
||||
}
|
||||
JS_PopGCRef (ctx, &elem_ref);
|
||||
nota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
|
||||
JSValue adata = JS_NULL;
|
||||
if (!JS_IsNull (ctx->actor_sym)) {
|
||||
int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
|
||||
if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
|
||||
}
|
||||
if (!JS_IsNull (adata)) {
|
||||
nota_write_sym (&enc->nb, NOTA_PRIVATE);
|
||||
nota_encode_value (enc, adata, replaced_ref.val, JS_NULL);
|
||||
JS_FreeValue (ctx, adata);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, adata);
|
||||
if (nota_stack_has (enc, replaced_ref.val)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
nota_stack_push (enc, replaced_ref.val);
|
||||
|
||||
JSValue to_json = JS_GetPropertyStr (ctx, replaced_ref.val, "toJSON");
|
||||
if (JS_IsFunction (to_json)) {
|
||||
JSValue result = JS_Call (ctx, to_json, replaced_ref.val, 0, NULL);
|
||||
if (!JS_IsException (result)) {
|
||||
nota_encode_value (enc, result, holder, key);
|
||||
} else {
|
||||
nota_write_sym (&enc->nb, NOTA_NULL);
|
||||
}
|
||||
nota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
|
||||
JS_PushGCRef (ctx, &keys_ref);
|
||||
keys_ref.val = JS_GetOwnPropertyNames (ctx, replaced_ref.val);
|
||||
if (JS_IsException (keys_ref.val)) {
|
||||
nota_write_sym (&enc->nb, NOTA_NULL);
|
||||
nota_stack_pop (enc);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
break;
|
||||
}
|
||||
int64_t plen64;
|
||||
if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
|
||||
nota_write_sym (&enc->nb, NOTA_NULL);
|
||||
nota_stack_pop (enc);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
break;
|
||||
}
|
||||
uint32_t plen = (uint32_t)plen64;
|
||||
|
||||
JS_PushGCRef (ctx, &prop_ref);
|
||||
JS_PushGCRef (ctx, &elem_ref);
|
||||
uint32_t non_function_count = 0;
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
|
||||
prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
|
||||
if (!JS_IsFunction (prop_ref.val)) non_function_count++;
|
||||
}
|
||||
|
||||
nota_write_record (&enc->nb, non_function_count);
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
|
||||
prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
|
||||
if (!JS_IsFunction (prop_ref.val)) {
|
||||
const char *prop_name = JS_ToCString (ctx, elem_ref.val);
|
||||
nota_write_text (&enc->nb, prop_name ? prop_name : "");
|
||||
nota_encode_value (enc, prop_ref.val, replaced_ref.val, elem_ref.val);
|
||||
JS_FreeCString (ctx, prop_name);
|
||||
}
|
||||
}
|
||||
JS_PopGCRef (ctx, &elem_ref);
|
||||
JS_PopGCRef (ctx, &prop_ref);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
nota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
nota_write_sym (&enc->nb, NOTA_NULL);
|
||||
break;
|
||||
}
|
||||
JS_PopGCRef (ctx, &replaced_ref);
|
||||
}
|
||||
|
||||
void *value2nota (JSContext *ctx, JSValue v) {
|
||||
JSGCRef val_ref, key_ref;
|
||||
JS_PushGCRef (ctx, &val_ref);
|
||||
JS_PushGCRef (ctx, &key_ref);
|
||||
val_ref.val = v;
|
||||
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visited_list = NULL;
|
||||
enc->cycle = 0;
|
||||
enc->replacer_ref = NULL;
|
||||
|
||||
nota_buffer_init (&enc->nb, 128);
|
||||
key_ref.val = JS_NewString (ctx, "");
|
||||
nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);
|
||||
|
||||
if (enc->cycle) {
|
||||
JS_PopGCRef (ctx, &key_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
nota_buffer_free (&enc->nb);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JS_PopGCRef (ctx, &key_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
void *data_ptr = enc->nb.data;
|
||||
enc->nb.data = NULL;
|
||||
nota_buffer_free (&enc->nb);
|
||||
return data_ptr;
|
||||
}
|
||||
|
||||
JSValue nota2value (JSContext *js, void *nota) {
|
||||
if (!nota) return JS_NULL;
|
||||
JSGCRef holder_ref, key_ref, ret_ref;
|
||||
JS_PushGCRef (js, &holder_ref);
|
||||
JS_PushGCRef (js, &key_ref);
|
||||
JS_PushGCRef (js, &ret_ref);
|
||||
holder_ref.val = JS_NewObject (js);
|
||||
key_ref.val = JS_NewString (js, "");
|
||||
ret_ref.val = JS_NULL;
|
||||
js_do_nota_decode (js, &ret_ref.val, nota, holder_ref.val, key_ref.val, JS_NULL);
|
||||
JSValue result = ret_ref.val;
|
||||
JS_PopGCRef (js, &ret_ref);
|
||||
JS_PopGCRef (js, &key_ref);
|
||||
JS_PopGCRef (js, &holder_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_RaiseDisrupt (ctx, "nota.encode requires at least 1 argument");
|
||||
|
||||
JSGCRef val_ref, replacer_ref, key_ref;
|
||||
JS_PushGCRef (ctx, &val_ref);
|
||||
JS_PushGCRef (ctx, &replacer_ref);
|
||||
JS_PushGCRef (ctx, &key_ref);
|
||||
val_ref.val = argv[0];
|
||||
replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
|
||||
|
||||
NotaEncodeContext enc_s, *enc = &enc_s;
|
||||
enc->ctx = ctx;
|
||||
enc->visited_list = NULL;
|
||||
enc->cycle = 0;
|
||||
enc->replacer_ref = &replacer_ref;
|
||||
|
||||
nota_buffer_init (&enc->nb, 128);
|
||||
key_ref.val = JS_NewString (ctx, "");
|
||||
nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);
|
||||
|
||||
JSValue ret;
|
||||
if (enc->cycle) {
|
||||
nota_buffer_free (&enc->nb);
|
||||
ret = JS_RaiseDisrupt (ctx, "Tried to encode something to nota with a cycle.");
|
||||
} else {
|
||||
size_t total_len = enc->nb.size;
|
||||
void *data_ptr = enc->nb.data;
|
||||
ret = js_new_blob_stoned_copy (ctx, (uint8_t *)data_ptr, total_len);
|
||||
nota_buffer_free (&enc->nb);
|
||||
}
|
||||
|
||||
JS_PopGCRef (ctx, &key_ref);
|
||||
JS_PopGCRef (ctx, &replacer_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static JSValue js_nota_decode (JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_NULL;
|
||||
|
||||
size_t len;
|
||||
unsigned char *nota = js_get_blob_data (js, &len, argv[0]);
|
||||
if (nota == (unsigned char *)-1) return JS_EXCEPTION;
|
||||
if (!nota) return JS_NULL;
|
||||
|
||||
JSGCRef holder_ref, key_ref, ret_ref, reviver_ref;
|
||||
JS_PushGCRef (js, &holder_ref);
|
||||
JS_PushGCRef (js, &key_ref);
|
||||
JS_PushGCRef (js, &ret_ref);
|
||||
JS_PushGCRef (js, &reviver_ref);
|
||||
|
||||
reviver_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
|
||||
holder_ref.val = JS_NewObject (js);
|
||||
key_ref.val = JS_NewString (js, "");
|
||||
ret_ref.val = JS_NULL;
|
||||
|
||||
js_do_nota_decode (js, &ret_ref.val, (char *)nota, holder_ref.val, key_ref.val, reviver_ref.val);
|
||||
|
||||
JSValue result = ret_ref.val;
|
||||
JS_PopGCRef (js, &reviver_ref);
|
||||
JS_PopGCRef (js, &ret_ref);
|
||||
JS_PopGCRef (js, &key_ref);
|
||||
JS_PopGCRef (js, &holder_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_nota_funcs[] = {
|
||||
JS_CFUNC_DEF ("encode", 1, js_nota_encode),
|
||||
JS_CFUNC_DEF ("decode", 1, js_nota_decode),
|
||||
};
|
||||
|
||||
JSValue js_core_internal_nota_use (JSContext *js) {
|
||||
JSGCRef export_ref;
|
||||
JS_PushGCRef (js, &export_ref);
|
||||
export_ref.val = JS_NewObject (js);
|
||||
JS_SetPropertyFunctionList (js, export_ref.val, js_nota_funcs, sizeof (js_nota_funcs) / sizeof (JSCFunctionListEntry));
|
||||
JSValue result = export_ref.val;
|
||||
JS_PopGCRef (js, &export_ref);
|
||||
return result;
|
||||
}
|
||||
@@ -34,14 +34,10 @@
|
||||
static JSClassID js_dylib_class_id;
|
||||
|
||||
static void js_dylib_finalizer(JSRuntime *rt, JSValue val) {
|
||||
void *handle = JS_GetOpaque(val, js_dylib_class_id);
|
||||
if (handle) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary((HMODULE)handle);
|
||||
#else
|
||||
dlclose(handle);
|
||||
#endif
|
||||
}
|
||||
/* Do NOT dlclose here. Loaded dylibs contain finalizer functions for other
|
||||
JS objects; if the dylib is freed before those objects during
|
||||
JS_FreeContext teardown, calling their finalizers would SEGV.
|
||||
The OS reclaims all loaded libraries on process exit. */
|
||||
}
|
||||
|
||||
static JSClassDef js_dylib_class = {
|
||||
@@ -740,7 +736,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
MIST_FUNC_DEF(os, stack, 1),
|
||||
};
|
||||
|
||||
JSValue js_core_os_use(JSContext *js) {
|
||||
JSValue js_core_internal_os_use(JSContext *js) {
|
||||
JS_NewClassID(&js_dylib_class_id);
|
||||
JS_NewClass(js, js_dylib_class_id, &js_dylib_class);
|
||||
|
||||
|
||||
469
internal/qop.c
Normal file
469
internal/qop.c
Normal file
@@ -0,0 +1,469 @@
|
||||
#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(qop->hashmap);
|
||||
}
|
||||
qop_close(qop);
|
||||
js_free_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(w->files);
|
||||
js_free_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_rt(qop->hashmap_size);
|
||||
if (!buffer) return 0;
|
||||
int num = qop_read_index(qop, buffer);
|
||||
if (num == 0) {
|
||||
js_free_rt(buffer);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
JSC_CCALL(qop_open,
|
||||
size_t len;
|
||||
void *data = js_get_blob_data(js, &len, argv[0]);
|
||||
if (data == (void *)-1)
|
||||
ret = JS_EXCEPTION;
|
||||
else if (!data)
|
||||
ret = JS_RaiseDisrupt(js, "Empty blob");
|
||||
else {
|
||||
qop_desc *qop = js_malloc_rt(sizeof(qop_desc));
|
||||
if (!qop)
|
||||
ret = JS_RaiseOOM(js);
|
||||
else {
|
||||
int size = qop_open_data((const unsigned char *)data, len, qop);
|
||||
if (size == 0) {
|
||||
js_free_rt(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_RaiseDisrupt(js, "Could not open file for writing");
|
||||
|
||||
qop_writer *w = js_malloc_rt(sizeof(qop_writer));
|
||||
if (!w) {
|
||||
fclose(fh);
|
||||
return JS_RaiseOOM(js);
|
||||
}
|
||||
|
||||
w->fh = fh;
|
||||
w->capacity = 1024;
|
||||
w->len = 0;
|
||||
w->size = 0;
|
||||
w->files = js_malloc_rt(sizeof(qop_file) * w->capacity);
|
||||
if (!w->files) {
|
||||
fclose(fh);
|
||||
js_free_rt(w);
|
||||
return JS_RaiseOOM(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_RaiseDisrupt(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_RaiseDisrupt(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_RaiseDisrupt(js, "Failed to read QOP index");
|
||||
}
|
||||
|
||||
qop_file *file = qop_find(qop, path);
|
||||
JS_FreeCString(js, path);
|
||||
|
||||
if (!file) {
|
||||
return JS_NULL;
|
||||
}
|
||||
|
||||
void *out;
|
||||
JSValue blob = js_new_blob_alloc(js, file->size, &out);
|
||||
if (JS_IsException(blob)) return blob;
|
||||
|
||||
int bytes = qop_read(qop, file, out);
|
||||
if (bytes == 0)
|
||||
return JS_RaiseDisrupt(js, "Failed to read file");
|
||||
|
||||
js_blob_stone(blob, bytes);
|
||||
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_RaiseDisrupt(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_RaiseDisrupt(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_RaiseDisrupt(js, "Invalid start or len");
|
||||
|
||||
void *out;
|
||||
JSValue blob = js_new_blob_alloc(js, len, &out);
|
||||
if (JS_IsException(blob)) return blob;
|
||||
|
||||
int bytes = qop_read_ex(qop, file, out, start, len);
|
||||
if (bytes == 0)
|
||||
return JS_RaiseDisrupt(js, "Failed to read file part");
|
||||
|
||||
js_blob_stone(blob, bytes);
|
||||
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_RaiseDisrupt(js, "Invalid QOP archive");
|
||||
|
||||
if (!js_qop_ensure_index(js, qop)) {
|
||||
return JS_RaiseDisrupt(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_rt(file->path_len);
|
||||
if (!path) {
|
||||
return JS_RaiseOOM(js);
|
||||
}
|
||||
|
||||
int len = qop_read_path(qop, file, path);
|
||||
if (len == 0) {
|
||||
js_free_rt(path);
|
||||
continue; // skip on error
|
||||
}
|
||||
|
||||
JSValue str = JS_NewStringLen(js, path, len - 1); // -1 for null terminator
|
||||
js_free_rt(path);
|
||||
JS_SetPropertyNumber(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_RaiseDisrupt(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_RaiseDisrupt(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_RaiseDisrupt(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_RaiseDisrupt(js, "Failed to read QOP index");
|
||||
}
|
||||
|
||||
// Check if any file starts with path + "/"
|
||||
size_t path_len = strlen(path);
|
||||
char *prefix = alloca(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_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_RaiseDisrupt(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_RaiseDisrupt(js, "No blob data present");
|
||||
}
|
||||
|
||||
if (w->len >= w->capacity) {
|
||||
w->capacity *= 2;
|
||||
qop_file *new_files = realloc(w->files, sizeof(qop_file) * w->capacity);
|
||||
if (!new_files) {
|
||||
JS_FreeCString(js, path);
|
||||
return JS_RaiseOOM(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_RaiseDisrupt(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_core_internal_qop_use(JSContext *js) {
|
||||
JS_FRAME(js);
|
||||
JS_NewClassID(&js_qop_archive_class_id);
|
||||
JS_NewClass(js, js_qop_archive_class_id, &js_qop_archive_class);
|
||||
JS_ROOT(archive_proto, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, archive_proto.val, js_qop_archive_funcs, countof(js_qop_archive_funcs));
|
||||
JS_SetClassProto(js, js_qop_archive_class_id, archive_proto.val);
|
||||
|
||||
JS_NewClassID(&js_qop_writer_class_id);
|
||||
JS_NewClass(js, js_qop_writer_class_id, &js_qop_writer_class);
|
||||
JS_ROOT(writer_proto, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, writer_proto.val, js_qop_writer_funcs, countof(js_qop_writer_funcs));
|
||||
JS_SetClassProto(js, js_qop_writer_class_id, writer_proto.val);
|
||||
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, mod.val, js_qop_funcs, countof(js_qop_funcs));
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
626
internal/shop.cm
626
internal/shop.cm
File diff suppressed because it is too large
Load Diff
@@ -33,26 +33,54 @@ function get_pkg_dir(package_name) {
|
||||
return shop.get_package_dir(package_name)
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
function ensure_dir(path) {
|
||||
if (fd.is_dir(path)) return true
|
||||
|
||||
var parts = array(path, '/')
|
||||
var current = starts_with(path, '/') ? '/' : ''
|
||||
// Deep comparison of two values (handles arrays and objects)
|
||||
function values_equal(a, b) {
|
||||
var i = 0
|
||||
for (i = 0; i < length(parts); i++) {
|
||||
if (parts[i] == '') continue
|
||||
current = current + parts[i] + '/'
|
||||
if (!fd.is_dir(current)) {
|
||||
fd.mkdir(current)
|
||||
var ka = null
|
||||
var kb = null
|
||||
if (a == b) return true
|
||||
if (is_null(a) && is_null(b)) return true
|
||||
if (is_null(a) || is_null(b)) return false
|
||||
if (is_array(a) && is_array(b)) {
|
||||
if (length(a) != length(b)) return false
|
||||
i = 0
|
||||
while (i < length(a)) {
|
||||
if (!values_equal(a[i], b[i])) return false
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true
|
||||
if (is_object(a) && is_object(b)) {
|
||||
ka = array(a)
|
||||
kb = array(b)
|
||||
if (length(ka) != length(kb)) return false
|
||||
i = 0
|
||||
while (i < length(ka)) {
|
||||
if (!values_equal(a[ka[i]], b[ka[i]])) return false
|
||||
i = i + 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Describe a value for error messages
|
||||
function describe(val) {
|
||||
if (is_null(val)) return "null"
|
||||
if (is_text(val)) return `"${val}"`
|
||||
if (is_number(val)) return text(val)
|
||||
if (is_logical(val)) return text(val)
|
||||
if (is_function(val)) return "<function>"
|
||||
if (is_array(val)) return `[array length=${text(length(val))}]`
|
||||
if (is_object(val)) return `{record keys=${text(length(array(val)))}}`
|
||||
return "<unknown>"
|
||||
}
|
||||
|
||||
return {
|
||||
is_valid_package: is_valid_package,
|
||||
get_current_package_name: get_current_package_name,
|
||||
get_pkg_dir: get_pkg_dir,
|
||||
ensure_dir: ensure_dir
|
||||
ensure_dir: fd.ensure_dir,
|
||||
values_equal: values_equal,
|
||||
describe: describe
|
||||
}
|
||||
|
||||
75
internal/time_playdate.c
Normal file
75
internal/time_playdate.c
Normal file
@@ -0,0 +1,75 @@
|
||||
// time_playdate.c - Time module for Playdate
|
||||
|
||||
#include "cell.h"
|
||||
#include "pd_api.h"
|
||||
|
||||
// Global Playdate API pointers - defined in main_playdate.c
|
||||
extern const struct playdate_sys *pd_sys;
|
||||
|
||||
/* ---------------------------------------------------------------- *\
|
||||
Helpers
|
||||
\* ---------------------------------------------------------------- */
|
||||
|
||||
static inline double playdate_now(void)
|
||||
{
|
||||
if (pd_sys) {
|
||||
unsigned int ms = 0;
|
||||
unsigned int secs = pd_sys->getSecondsSinceEpoch(&ms);
|
||||
return (double)secs + ms / 1000.0;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *\
|
||||
JS bindings
|
||||
\* ---------------------------------------------------------------- */
|
||||
|
||||
static JSValue
|
||||
js_time_now(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv)
|
||||
{
|
||||
return JS_NewFloat64(ctx, playdate_now());
|
||||
}
|
||||
|
||||
static JSValue
|
||||
js_time_computer_dst(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv)
|
||||
{
|
||||
// Playdate doesn't provide DST info directly
|
||||
return JS_NewBool(ctx, 0);
|
||||
}
|
||||
|
||||
static JSValue
|
||||
js_time_computer_zone(JSContext *ctx, JSValueConst this_val,
|
||||
int argc, JSValueConst *argv)
|
||||
{
|
||||
#if TARGET_EXTENSION
|
||||
if (pd_sys) {
|
||||
int32_t offset = pd_sys->getTimezoneOffset();
|
||||
// Playdate returns offset in seconds, convert to hours
|
||||
return JS_NewFloat64(ctx, (double)offset / 3600.0);
|
||||
}
|
||||
#endif
|
||||
return JS_NewFloat64(ctx, 0.0);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- *\
|
||||
registration
|
||||
\* ---------------------------------------------------------------- */
|
||||
|
||||
static const JSCFunctionListEntry js_time_funcs[] = {
|
||||
JS_CFUNC_DEF("now", 0, js_time_now),
|
||||
JS_CFUNC_DEF("computer_dst", 0, js_time_computer_dst),
|
||||
JS_CFUNC_DEF("computer_zone", 0, js_time_computer_zone),
|
||||
};
|
||||
|
||||
JSValue
|
||||
js_time_use(JSContext *ctx)
|
||||
{
|
||||
JSValue obj = JS_NewObject(ctx);
|
||||
JS_SetPropertyFunctionList(ctx, obj,
|
||||
js_time_funcs,
|
||||
sizeof(js_time_funcs) /
|
||||
sizeof(js_time_funcs[0]));
|
||||
return obj;
|
||||
}
|
||||
36
internal/wildstar.c
Normal file
36
internal/wildstar.c
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "cell.h"
|
||||
#include "wildmatch.h"
|
||||
|
||||
JSC_CCALL(wildstar_match,
|
||||
const char *pattern = JS_ToCString(js, argv[0]);
|
||||
const char *string = JS_ToCString(js, argv[1]);
|
||||
int flags = 0;
|
||||
if (argc > 2)
|
||||
flags = js2number(js, argv[2]);
|
||||
|
||||
int result = wildmatch(pattern, string, flags);
|
||||
|
||||
JS_FreeCString(js, pattern);
|
||||
JS_FreeCString(js, string);
|
||||
|
||||
return JS_NewBool(js, result == WM_MATCH);
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_wildstar_funcs[] = {
|
||||
MIST_FUNC_DEF(wildstar, match, 3),
|
||||
JS_PROP_INT32_DEF("WM_MATCH", WM_MATCH, 0),
|
||||
JS_PROP_INT32_DEF("WM_NOMATCH", WM_NOMATCH, 0),
|
||||
JS_PROP_INT32_DEF("WM_NOESCAPE", WM_NOESCAPE, 0),
|
||||
JS_PROP_INT32_DEF("WM_PATHNAME", WM_PATHNAME, 0),
|
||||
JS_PROP_INT32_DEF("WM_PERIOD", WM_PERIOD, 0),
|
||||
JS_PROP_INT32_DEF("WM_LEADING_DIR", WM_LEADING_DIR, 0),
|
||||
JS_PROP_INT32_DEF("WM_CASEFOLD", WM_CASEFOLD, 0),
|
||||
JS_PROP_INT32_DEF("WM_WILDSTAR", WM_WILDSTAR, 0),
|
||||
};
|
||||
|
||||
JSValue js_core_internal_wildstar_use(JSContext *js) {
|
||||
JS_FRAME(js);
|
||||
JS_ROOT(mod, JS_NewObject(js));
|
||||
JS_SetPropertyFunctionList(js, mod.val, js_wildstar_funcs, countof(js_wildstar_funcs));
|
||||
JS_RETURN(mod.val);
|
||||
}
|
||||
461
internal/wota.c
Normal file
461
internal/wota.c
Normal file
@@ -0,0 +1,461 @@
|
||||
#define WOTA_IMPLEMENTATION
|
||||
#include "quickjs-internal.h"
|
||||
#include "cell.h"
|
||||
|
||||
typedef struct ObjectRef {
|
||||
void *ptr;
|
||||
struct ObjectRef *next;
|
||||
} ObjectRef;
|
||||
|
||||
typedef struct WotaEncodeContext {
|
||||
JSContext *ctx;
|
||||
ObjectRef *visited_stack;
|
||||
WotaBuffer wb;
|
||||
int cycle;
|
||||
JSValue replacer;
|
||||
} WotaEncodeContext;
|
||||
|
||||
static void wota_stack_push (WotaEncodeContext *enc, JSValueConst val) {
|
||||
(void)enc; (void)val;
|
||||
/* Cycle detection disabled for performance */
|
||||
}
|
||||
|
||||
static void wota_stack_pop (WotaEncodeContext *enc) {
|
||||
if (!enc->visited_stack) return;
|
||||
|
||||
ObjectRef *top = enc->visited_stack;
|
||||
enc->visited_stack = top->next;
|
||||
sys_free (top);
|
||||
}
|
||||
|
||||
static int wota_stack_has (WotaEncodeContext *enc, JSValueConst val) {
|
||||
(void)enc; (void)val;
|
||||
return 0;
|
||||
/* Cycle detection disabled for performance */
|
||||
}
|
||||
|
||||
static void wota_stack_free (WotaEncodeContext *enc) {
|
||||
while (enc->visited_stack) {
|
||||
wota_stack_pop (enc);
|
||||
}
|
||||
}
|
||||
|
||||
static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) {
|
||||
if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val);
|
||||
JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key);
|
||||
JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) };
|
||||
JSValue result = JS_Call (enc->ctx, enc->replacer, holder, 2, args);
|
||||
JS_FreeValue (enc->ctx, args[0]);
|
||||
JS_FreeValue (enc->ctx, args[1]);
|
||||
if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key);
|
||||
|
||||
static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) {
|
||||
JSContext *ctx = enc->ctx;
|
||||
|
||||
/* Root the input value to protect it during property enumeration */
|
||||
JSGCRef val_ref, keys_ref;
|
||||
JS_PushGCRef (ctx, &val_ref);
|
||||
JS_PushGCRef (ctx, &keys_ref);
|
||||
val_ref.val = JS_DupValue (ctx, val);
|
||||
|
||||
keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val);
|
||||
if (JS_IsException (keys_ref.val)) {
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
JS_FreeValue (ctx, val_ref.val);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return;
|
||||
}
|
||||
int64_t plen64;
|
||||
if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
|
||||
JS_FreeValue (ctx, keys_ref.val);
|
||||
JS_FreeValue (ctx, val_ref.val);
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return;
|
||||
}
|
||||
uint32_t plen = (uint32_t)plen64;
|
||||
uint32_t non_function_count = 0;
|
||||
|
||||
/* Allocate GC-rooted arrays for props and keys */
|
||||
JSGCRef *prop_refs = sys_malloc (sizeof (JSGCRef) * plen);
|
||||
JSGCRef *key_refs = sys_malloc (sizeof (JSGCRef) * plen);
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
JS_PushGCRef (ctx, &prop_refs[i]);
|
||||
JS_PushGCRef (ctx, &key_refs[i]);
|
||||
prop_refs[i].val = JS_NULL;
|
||||
key_refs[i].val = JS_NULL;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < plen; i++) {
|
||||
/* Store key into its GCRef slot immediately so it's rooted before
|
||||
JS_GetProperty can trigger GC and relocate the string. */
|
||||
key_refs[i].val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
|
||||
JSValue prop_val = JS_GetProperty (ctx, val_ref.val, key_refs[i].val);
|
||||
if (!JS_IsFunction (prop_val)) {
|
||||
if (i != non_function_count) {
|
||||
key_refs[non_function_count].val = key_refs[i].val;
|
||||
key_refs[i].val = JS_NULL;
|
||||
}
|
||||
prop_refs[non_function_count].val = prop_val;
|
||||
non_function_count++;
|
||||
} else {
|
||||
JS_FreeValue (ctx, prop_val);
|
||||
JS_FreeValue (ctx, key_refs[i].val);
|
||||
key_refs[i].val = JS_NULL;
|
||||
}
|
||||
}
|
||||
JS_FreeValue (ctx, keys_ref.val);
|
||||
wota_write_record (&enc->wb, non_function_count);
|
||||
for (uint32_t i = 0; i < non_function_count; i++) {
|
||||
size_t klen;
|
||||
const char *prop_name = JS_ToCStringLen (ctx, &klen, key_refs[i].val);
|
||||
wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0);
|
||||
wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val);
|
||||
JS_FreeCString (ctx, prop_name);
|
||||
JS_FreeValue (ctx, prop_refs[i].val);
|
||||
JS_FreeValue (ctx, key_refs[i].val);
|
||||
}
|
||||
/* Pop all GC refs in reverse order */
|
||||
for (int i = plen - 1; i >= 0; i--) {
|
||||
JS_PopGCRef (ctx, &key_refs[i]);
|
||||
JS_PopGCRef (ctx, &prop_refs[i]);
|
||||
}
|
||||
sys_free (prop_refs);
|
||||
sys_free (key_refs);
|
||||
JS_FreeValue (ctx, val_ref.val);
|
||||
JS_PopGCRef (ctx, &keys_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
}
|
||||
|
||||
static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key) {
|
||||
JSContext *ctx = enc->ctx;
|
||||
JSValue replaced;
|
||||
if (!JS_IsNull (enc->replacer) && !JS_IsNull (key))
|
||||
replaced = wota_apply_replacer (enc, holder, key, val);
|
||||
else
|
||||
replaced = JS_DupValue (enc->ctx, val);
|
||||
|
||||
int tag = JS_VALUE_GET_TAG (replaced);
|
||||
switch (tag) {
|
||||
case JS_TAG_INT: {
|
||||
int32_t d;
|
||||
JS_ToInt32 (ctx, &d, replaced);
|
||||
wota_write_int_word (&enc->wb, d);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_FLOAT64: {
|
||||
double d;
|
||||
if (JS_ToFloat64 (ctx, &d, replaced) < 0) {
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
break;
|
||||
}
|
||||
wota_write_float_word (&enc->wb, d);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_STRING: {
|
||||
size_t plen;
|
||||
const char *str = JS_ToCStringLen (ctx, &plen, replaced);
|
||||
wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
|
||||
JS_FreeCString (ctx, str);
|
||||
break;
|
||||
}
|
||||
case JS_TAG_BOOL:
|
||||
wota_write_sym (&enc->wb, JS_VALUE_GET_BOOL (replaced) ? WOTA_TRUE : WOTA_FALSE);
|
||||
break;
|
||||
case JS_TAG_NULL:
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
break;
|
||||
case JS_TAG_PTR: {
|
||||
if (JS_IsText (replaced)) {
|
||||
size_t plen;
|
||||
const char *str = JS_ToCStringLen (ctx, &plen, replaced);
|
||||
wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
|
||||
JS_FreeCString (ctx, str);
|
||||
break;
|
||||
}
|
||||
if (js_is_blob (ctx, replaced)) {
|
||||
size_t buf_len;
|
||||
void *buf_data = js_get_blob_data (ctx, &buf_len, replaced);
|
||||
if (buf_data == (void *)-1) {
|
||||
JS_FreeValue (ctx, replaced);
|
||||
return;
|
||||
}
|
||||
if (buf_len == 0) {
|
||||
wota_write_blob (&enc->wb, 0, "");
|
||||
} else {
|
||||
wota_write_blob (&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (JS_IsArray (replaced)) {
|
||||
if (wota_stack_has (enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
wota_stack_push (enc, replaced);
|
||||
int64_t arr_len;
|
||||
JS_GetLength (ctx, replaced, &arr_len);
|
||||
wota_write_array (&enc->wb, arr_len);
|
||||
for (int64_t i = 0; i < arr_len; i++) {
|
||||
JSValue elem_val = JS_GetPropertyNumber (ctx, replaced, i);
|
||||
wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i));
|
||||
JS_FreeValue (ctx, elem_val);
|
||||
}
|
||||
wota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
JSValue adata = JS_NULL;
|
||||
if (!JS_IsNull (ctx->actor_sym)) {
|
||||
int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym);
|
||||
if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym);
|
||||
}
|
||||
if (!JS_IsNull (adata)) {
|
||||
wota_write_sym (&enc->wb, WOTA_PRIVATE);
|
||||
wota_encode_value (enc, adata, replaced, JS_NULL);
|
||||
JS_FreeValue (ctx, adata);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, adata);
|
||||
if (wota_stack_has (enc, replaced)) {
|
||||
enc->cycle = 1;
|
||||
break;
|
||||
}
|
||||
wota_stack_push (enc, replaced);
|
||||
JSValue to_json = JS_GetPropertyStr (ctx, replaced, "toJSON");
|
||||
if (JS_IsFunction (to_json)) {
|
||||
JSValue result = JS_Call (ctx, to_json, replaced, 0, NULL);
|
||||
JS_FreeValue (ctx, to_json);
|
||||
if (!JS_IsException (result)) {
|
||||
wota_encode_value (enc, result, holder, key);
|
||||
JS_FreeValue (ctx, result);
|
||||
} else
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
wota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, to_json);
|
||||
encode_object_properties (enc, replaced, holder);
|
||||
wota_stack_pop (enc);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
wota_write_sym (&enc->wb, WOTA_NULL);
|
||||
break;
|
||||
}
|
||||
JS_FreeValue (ctx, replaced);
|
||||
}
|
||||
|
||||
static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) {
|
||||
uint64_t first_word = *(uint64_t *)data_ptr;
|
||||
int type = (int)(first_word & 0xffU);
|
||||
switch (type) {
|
||||
case WOTA_INT: {
|
||||
long long val;
|
||||
data_ptr = wota_read_int (&val, data_ptr);
|
||||
*out_val = JS_NewInt64 (ctx, val);
|
||||
break;
|
||||
}
|
||||
case WOTA_FLOAT: {
|
||||
double d;
|
||||
data_ptr = wota_read_float (&d, data_ptr);
|
||||
*out_val = JS_NewFloat64 (ctx, d);
|
||||
break;
|
||||
}
|
||||
case WOTA_SYM: {
|
||||
int scode;
|
||||
data_ptr = wota_read_sym (&scode, data_ptr);
|
||||
if (scode == WOTA_PRIVATE) {
|
||||
JSGCRef inner_ref, obj_ref2;
|
||||
JS_PushGCRef (ctx, &inner_ref);
|
||||
JS_PushGCRef (ctx, &obj_ref2);
|
||||
inner_ref.val = JS_NULL;
|
||||
data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver);
|
||||
obj_ref2.val = JS_NewObject (ctx);
|
||||
if (!JS_IsNull (ctx->actor_sym))
|
||||
JS_SetPropertyKey (ctx, obj_ref2.val, ctx->actor_sym, inner_ref.val);
|
||||
JS_CellStone (ctx, obj_ref2.val);
|
||||
*out_val = obj_ref2.val;
|
||||
JS_PopGCRef (ctx, &obj_ref2);
|
||||
JS_PopGCRef (ctx, &inner_ref);
|
||||
} else if (scode == WOTA_NULL) *out_val = JS_NULL;
|
||||
else if (scode == WOTA_FALSE) *out_val = JS_NewBool (ctx, 0);
|
||||
else if (scode == WOTA_TRUE) *out_val = JS_NewBool (ctx, 1);
|
||||
else *out_val = JS_NULL;
|
||||
break;
|
||||
}
|
||||
case WOTA_BLOB: {
|
||||
long long blen;
|
||||
char *bdata = NULL;
|
||||
data_ptr = wota_read_blob (&blen, &bdata, data_ptr);
|
||||
*out_val = bdata ? js_new_blob_stoned_copy (ctx, (uint8_t *)bdata, (size_t)blen) : js_new_blob_stoned_copy (ctx, NULL, 0);
|
||||
if (bdata) sys_free (bdata);
|
||||
break;
|
||||
}
|
||||
case WOTA_TEXT: {
|
||||
char *utf8 = NULL;
|
||||
data_ptr = wota_read_text (&utf8, data_ptr);
|
||||
*out_val = JS_NewString (ctx, utf8 ? utf8 : "");
|
||||
if (utf8) sys_free (utf8);
|
||||
break;
|
||||
}
|
||||
case WOTA_ARR: {
|
||||
long long c;
|
||||
data_ptr = wota_read_array (&c, data_ptr);
|
||||
JSGCRef arr_ref;
|
||||
JS_PushGCRef (ctx, &arr_ref);
|
||||
arr_ref.val = JS_NewArrayLen (ctx, c);
|
||||
for (long long i = 0; i < c; i++) {
|
||||
JSGCRef elem_ref;
|
||||
JS_PushGCRef (ctx, &elem_ref);
|
||||
elem_ref.val = JS_NULL;
|
||||
JSValue idx_key = JS_NewInt32 (ctx, (int32_t)i);
|
||||
data_ptr = decode_wota_value (ctx, data_ptr, &elem_ref.val, arr_ref.val, idx_key, reviver);
|
||||
JS_SetPropertyNumber (ctx, arr_ref.val, i, elem_ref.val);
|
||||
JS_PopGCRef (ctx, &elem_ref);
|
||||
}
|
||||
*out_val = arr_ref.val;
|
||||
JS_PopGCRef (ctx, &arr_ref);
|
||||
break;
|
||||
}
|
||||
case WOTA_REC: {
|
||||
long long c;
|
||||
data_ptr = wota_read_record (&c, data_ptr);
|
||||
JSGCRef obj_ref;
|
||||
JS_PushGCRef (ctx, &obj_ref);
|
||||
obj_ref.val = JS_NewObject (ctx);
|
||||
for (long long i = 0; i < c; i++) {
|
||||
char *tkey = NULL;
|
||||
size_t key_len;
|
||||
data_ptr = wota_read_text_len (&key_len, &tkey, data_ptr);
|
||||
if (!tkey) continue;
|
||||
JSGCRef prop_key_ref, sub_val_ref;
|
||||
JS_PushGCRef (ctx, &prop_key_ref);
|
||||
JS_PushGCRef (ctx, &sub_val_ref);
|
||||
prop_key_ref.val = JS_NewStringLen (ctx, tkey, key_len);
|
||||
sub_val_ref.val = JS_NULL;
|
||||
data_ptr = decode_wota_value (ctx, data_ptr, &sub_val_ref.val, obj_ref.val, prop_key_ref.val, reviver);
|
||||
JS_SetPropertyStr (ctx, obj_ref.val, tkey, sub_val_ref.val);
|
||||
JS_PopGCRef (ctx, &sub_val_ref);
|
||||
JS_PopGCRef (ctx, &prop_key_ref);
|
||||
sys_free (tkey);
|
||||
}
|
||||
*out_val = obj_ref.val;
|
||||
JS_PopGCRef (ctx, &obj_ref);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
data_ptr += 8;
|
||||
*out_val = JS_NULL;
|
||||
break;
|
||||
}
|
||||
if (!JS_IsNull (reviver)) {
|
||||
JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key);
|
||||
JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) };
|
||||
JSValue revived = JS_Call (ctx, reviver, holder, 2, args);
|
||||
JS_FreeValue (ctx, args[0]);
|
||||
JS_FreeValue (ctx, args[1]);
|
||||
if (!JS_IsException (revived)) {
|
||||
JS_FreeValue (ctx, *out_val);
|
||||
*out_val = revived;
|
||||
} else
|
||||
JS_FreeValue (ctx, revived);
|
||||
}
|
||||
return data_ptr;
|
||||
}
|
||||
|
||||
void *value2wota (JSContext *ctx, JSValue v, JSValue replacer, size_t *bytes) {
|
||||
JSGCRef val_ref, rep_ref;
|
||||
JS_PushGCRef (ctx, &val_ref);
|
||||
JS_PushGCRef (ctx, &rep_ref);
|
||||
val_ref.val = v;
|
||||
rep_ref.val = replacer;
|
||||
|
||||
WotaEncodeContext enc_s, *enc = &enc_s;
|
||||
|
||||
enc->ctx = ctx;
|
||||
enc->visited_stack = NULL;
|
||||
enc->cycle = 0;
|
||||
enc->replacer = rep_ref.val;
|
||||
wota_buffer_init (&enc->wb, 16);
|
||||
wota_encode_value (enc, val_ref.val, JS_NULL, JS_NULL);
|
||||
if (enc->cycle) {
|
||||
wota_stack_free (enc);
|
||||
wota_buffer_free (&enc->wb);
|
||||
JS_PopGCRef (ctx, &rep_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return NULL;
|
||||
}
|
||||
wota_stack_free (enc);
|
||||
size_t total_bytes = enc->wb.size * sizeof (uint64_t);
|
||||
void *wota = sys_realloc (enc->wb.data, total_bytes);
|
||||
if (bytes) *bytes = total_bytes;
|
||||
JS_PopGCRef (ctx, &rep_ref);
|
||||
JS_PopGCRef (ctx, &val_ref);
|
||||
return wota;
|
||||
}
|
||||
|
||||
JSValue wota2value (JSContext *ctx, void *wota) {
|
||||
JSGCRef holder_ref, result_ref;
|
||||
JS_PushGCRef (ctx, &holder_ref);
|
||||
JS_PushGCRef (ctx, &result_ref);
|
||||
result_ref.val = JS_NULL;
|
||||
holder_ref.val = JS_NewObject (ctx);
|
||||
decode_wota_value (ctx, wota, &result_ref.val, holder_ref.val, JS_NULL, JS_NULL);
|
||||
JSValue result = result_ref.val;
|
||||
JS_PopGCRef (ctx, &result_ref);
|
||||
JS_PopGCRef (ctx, &holder_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
static JSValue js_wota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_RaiseDisrupt (ctx, "wota.encode requires at least 1 argument");
|
||||
size_t total_bytes;
|
||||
void *wota = value2wota (ctx, argv[0], JS_IsFunction (argv[1]) ? argv[1] : JS_NULL, &total_bytes);
|
||||
JSValue ret = js_new_blob_stoned_copy (ctx, wota, total_bytes);
|
||||
sys_free (wota);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static JSValue js_wota_decode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
|
||||
if (argc < 1) return JS_NULL;
|
||||
size_t len;
|
||||
uint8_t *buf = js_get_blob_data (ctx, &len, argv[0]);
|
||||
if (buf == (uint8_t *)-1) return JS_EXCEPTION;
|
||||
if (!buf || len == 0) return JS_RaiseDisrupt (ctx, "No blob data present");
|
||||
JSValue reviver = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
|
||||
char *data_ptr = (char *)buf;
|
||||
JSGCRef result_ref, holder_ref, empty_key_ref;
|
||||
JS_PushGCRef (ctx, &result_ref);
|
||||
JS_PushGCRef (ctx, &holder_ref);
|
||||
JS_PushGCRef (ctx, &empty_key_ref);
|
||||
result_ref.val = JS_NULL;
|
||||
holder_ref.val = JS_NewObject (ctx);
|
||||
empty_key_ref.val = JS_NewString (ctx, "");
|
||||
decode_wota_value (ctx, data_ptr, &result_ref.val, holder_ref.val, empty_key_ref.val, reviver);
|
||||
JSValue result = result_ref.val;
|
||||
JS_PopGCRef (ctx, &empty_key_ref);
|
||||
JS_PopGCRef (ctx, &holder_ref);
|
||||
JS_PopGCRef (ctx, &result_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_wota_funcs[] = {
|
||||
JS_CFUNC_DEF ("encode", 2, js_wota_encode),
|
||||
JS_CFUNC_DEF ("decode", 2, js_wota_decode),
|
||||
};
|
||||
|
||||
JSValue js_core_internal_wota_use (JSContext *ctx) {
|
||||
JSGCRef exports_ref;
|
||||
JS_PushGCRef (ctx, &exports_ref);
|
||||
exports_ref.val = JS_NewObject (ctx);
|
||||
JS_SetPropertyFunctionList (ctx, exports_ref.val, js_wota_funcs, sizeof (js_wota_funcs) / sizeof (js_wota_funcs[0]));
|
||||
JSValue result = exports_ref.val;
|
||||
JS_PopGCRef (ctx, &exports_ref);
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user