mirror of
https://github.com/flipperdevices/flipperzero-firmware.git
synced 2025-12-13 13:29:50 +04:00
[FL-3579, FL-3601, FL-3714] JavaScript runner (#3286)
* FBT: cdefines to env, libs order * API: strtod, modf, itoa, calloc * Apps: elk js * Apps: mjs * JS: scripts as assets * mjs: composite resolver * mjs: stack trace * ELK JS example removed * MJS thread, MJS lib modified to support script interruption * JS console UI * Module system, BadUSB bindings rework * JS notifications, simple dialog, BadUSB demo * Custom dialogs, dialog demo * MJS as system library, some dirty hacks to make it compile * Plugin-based js modules * js_uart(BadUART) module * js_uart: support for byte array arguments * Script icon and various fixes * File browser: multiple extensions filter, running js scripts from app loader * Running js scripts from archive browser * JS Runner as system app * Example scripts moved to /ext/apps/Scripts * JS bytecode listing generation * MJS builtin printf cleanup * JS examples cleanup * mbedtls version fix * Unused lib cleanup * Making PVS happy & TODOs cleanup * TODOs cleanup #2 * MJS: initial typed arrays support * JS: fix mem leak in uart destructor Co-authored-by: SG <who.just.the.doctor@gmail.com> Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
523
lib/mjs/mjs_json.c
Normal file
523
lib/mjs/mjs_json.c
Normal file
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* Copyright (c) 2016 Cesanta Software Limited
|
||||
* All rights reserved
|
||||
*/
|
||||
|
||||
#include "common/str_util.h"
|
||||
#include "common/frozen/frozen.h"
|
||||
#include "mjs_array.h"
|
||||
#include "mjs_internal.h"
|
||||
#include "mjs_core.h"
|
||||
#include "mjs_object.h"
|
||||
#include "mjs_primitive.h"
|
||||
#include "mjs_string.h"
|
||||
#include "mjs_util_public.h"
|
||||
|
||||
#define BUF_LEFT(size, used) (((size_t)(used) < (size)) ? ((size) - (used)) : 0)
|
||||
|
||||
/*
|
||||
* Returns whether the value of given type should be skipped when generating
|
||||
* JSON output
|
||||
*
|
||||
* So far it always returns 0, but we might add some logic later, if we
|
||||
* implement some non-jsonnable objects
|
||||
*/
|
||||
static int should_skip_for_json(enum mjs_type type) {
|
||||
int ret;
|
||||
switch(type) {
|
||||
/* All permitted values */
|
||||
case MJS_TYPE_NULL:
|
||||
case MJS_TYPE_BOOLEAN:
|
||||
case MJS_TYPE_NUMBER:
|
||||
case MJS_TYPE_STRING:
|
||||
case MJS_TYPE_ARRAY_BUF:
|
||||
case MJS_TYPE_ARRAY_BUF_VIEW:
|
||||
case MJS_TYPE_OBJECT_GENERIC:
|
||||
case MJS_TYPE_OBJECT_ARRAY:
|
||||
ret = 0;
|
||||
break;
|
||||
default:
|
||||
ret = 1;
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static const char* hex_digits = "0123456789abcdef";
|
||||
static char* append_hex(char* buf, char* limit, uint8_t c) {
|
||||
if(buf < limit) *buf++ = 'u';
|
||||
if(buf < limit) *buf++ = '0';
|
||||
if(buf < limit) *buf++ = '0';
|
||||
if(buf < limit) *buf++ = hex_digits[(int)((c >> 4) % 0xf)];
|
||||
if(buf < limit) *buf++ = hex_digits[(int)(c & 0xf)];
|
||||
return buf;
|
||||
}
|
||||
|
||||
/*
|
||||
* Appends quoted s to buf. Any double quote contained in s will be escaped.
|
||||
* Returns the number of characters that would have been added,
|
||||
* like snprintf.
|
||||
* If size is zero it doesn't output anything but keeps counting.
|
||||
*/
|
||||
static int snquote(char* buf, size_t size, const char* s, size_t len) {
|
||||
char* limit = buf + size;
|
||||
const char* end;
|
||||
/*
|
||||
* String single character escape sequence:
|
||||
* http://www.ecma-international.org/ecma-262/6.0/index.html#table-34
|
||||
*
|
||||
* 0x8 -> \b
|
||||
* 0x9 -> \t
|
||||
* 0xa -> \n
|
||||
* 0xb -> \v
|
||||
* 0xc -> \f
|
||||
* 0xd -> \r
|
||||
*/
|
||||
const char* specials = "btnvfr";
|
||||
size_t i = 0;
|
||||
|
||||
i++;
|
||||
if(buf < limit) *buf++ = '"';
|
||||
|
||||
for(end = s + len; s < end; s++) {
|
||||
if(*s == '"' || *s == '\\') {
|
||||
i++;
|
||||
if(buf < limit) *buf++ = '\\';
|
||||
} else if(*s >= '\b' && *s <= '\r') {
|
||||
i += 2;
|
||||
if(buf < limit) *buf++ = '\\';
|
||||
if(buf < limit) *buf++ = specials[*s - '\b'];
|
||||
continue;
|
||||
} else if((unsigned char)*s < '\b' || (*s > '\r' && *s < ' ')) {
|
||||
i += 6 /* \uXX XX */;
|
||||
if(buf < limit) *buf++ = '\\';
|
||||
buf = append_hex(buf, limit, (uint8_t)*s);
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
if(buf < limit) *buf++ = *s;
|
||||
}
|
||||
|
||||
i++;
|
||||
if(buf < limit) *buf++ = '"';
|
||||
|
||||
if(buf < limit) {
|
||||
*buf = '\0';
|
||||
} else if(size != 0) {
|
||||
/*
|
||||
* There is no room for the NULL char, but the size wasn't zero, so we can
|
||||
* safely put NULL in the previous byte
|
||||
*/
|
||||
*(buf - 1) = '\0';
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
MJS_PRIVATE mjs_err_t to_json_or_debug(
|
||||
struct mjs* mjs,
|
||||
mjs_val_t v,
|
||||
char* buf,
|
||||
size_t size,
|
||||
size_t* res_len,
|
||||
uint8_t is_debug) {
|
||||
mjs_val_t el;
|
||||
char* vp;
|
||||
mjs_err_t rcode = MJS_OK;
|
||||
size_t len = 0;
|
||||
/*
|
||||
* TODO(dfrank) : also push all `mjs_val_t`s that are declared below
|
||||
*/
|
||||
|
||||
if(size > 0) *buf = '\0';
|
||||
|
||||
if(!is_debug && should_skip_for_json(mjs_get_type(v))) {
|
||||
goto clean;
|
||||
}
|
||||
|
||||
for(vp = mjs->json_visited_stack.buf;
|
||||
vp < mjs->json_visited_stack.buf + mjs->json_visited_stack.len;
|
||||
vp += sizeof(mjs_val_t)) {
|
||||
if(*(mjs_val_t*)vp == v) {
|
||||
strncpy(buf, "[Circular]", size);
|
||||
len = 10;
|
||||
goto clean;
|
||||
}
|
||||
}
|
||||
|
||||
switch(mjs_get_type(v)) {
|
||||
case MJS_TYPE_NULL:
|
||||
case MJS_TYPE_BOOLEAN:
|
||||
case MJS_TYPE_NUMBER:
|
||||
case MJS_TYPE_UNDEFINED:
|
||||
case MJS_TYPE_FOREIGN:
|
||||
case MJS_TYPE_ARRAY_BUF:
|
||||
case MJS_TYPE_ARRAY_BUF_VIEW:
|
||||
/* For those types, regular `mjs_to_string()` works */
|
||||
{
|
||||
/* refactor: mjs_to_string allocates memory every time */
|
||||
char* p = NULL;
|
||||
int need_free = 0;
|
||||
rcode = mjs_to_string(mjs, &v, &p, &len, &need_free);
|
||||
c_snprintf(buf, size, "%.*s", (int)len, p);
|
||||
if(need_free) {
|
||||
free(p);
|
||||
}
|
||||
}
|
||||
goto clean;
|
||||
|
||||
case MJS_TYPE_STRING: {
|
||||
/*
|
||||
* For strings we can't just use `primitive_to_str()`, because we need
|
||||
* quoted value
|
||||
*/
|
||||
size_t n;
|
||||
const char* str = mjs_get_string(mjs, &v, &n);
|
||||
len = snquote(buf, size, str, n);
|
||||
goto clean;
|
||||
}
|
||||
|
||||
case MJS_TYPE_OBJECT_FUNCTION:
|
||||
case MJS_TYPE_OBJECT_GENERIC: {
|
||||
char* b = buf;
|
||||
struct mjs_property* prop = NULL;
|
||||
struct mjs_object* o = NULL;
|
||||
|
||||
mbuf_append(&mjs->json_visited_stack, (char*)&v, sizeof(v));
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "{");
|
||||
o = get_object_struct(v);
|
||||
for(prop = o->properties; prop != NULL; prop = prop->next) {
|
||||
size_t n;
|
||||
const char* s;
|
||||
if(!is_debug && should_skip_for_json(mjs_get_type(prop->value))) {
|
||||
continue;
|
||||
}
|
||||
if(b - buf != 1) { /* Not the first property to be printed */
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), ",");
|
||||
}
|
||||
s = mjs_get_string(mjs, &prop->name, &n);
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "\"%.*s\":", (int)n, s);
|
||||
{
|
||||
size_t tmp = 0;
|
||||
rcode =
|
||||
to_json_or_debug(mjs, prop->value, b, BUF_LEFT(size, b - buf), &tmp, is_debug);
|
||||
if(rcode != MJS_OK) {
|
||||
goto clean_iter;
|
||||
}
|
||||
b += tmp;
|
||||
}
|
||||
}
|
||||
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "}");
|
||||
mjs->json_visited_stack.len -= sizeof(v);
|
||||
|
||||
clean_iter:
|
||||
len = b - buf;
|
||||
goto clean;
|
||||
}
|
||||
case MJS_TYPE_OBJECT_ARRAY: {
|
||||
int has;
|
||||
char* b = buf;
|
||||
size_t i, alen = mjs_array_length(mjs, v);
|
||||
mbuf_append(&mjs->json_visited_stack, (char*)&v, sizeof(v));
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "[");
|
||||
for(i = 0; i < alen; i++) {
|
||||
el = mjs_array_get2(mjs, v, i, &has);
|
||||
if(has) {
|
||||
size_t tmp = 0;
|
||||
if(!is_debug && should_skip_for_json(mjs_get_type(el))) {
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "null");
|
||||
} else {
|
||||
rcode = to_json_or_debug(mjs, el, b, BUF_LEFT(size, b - buf), &tmp, is_debug);
|
||||
if(rcode != MJS_OK) {
|
||||
goto clean;
|
||||
}
|
||||
}
|
||||
b += tmp;
|
||||
} else {
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "null");
|
||||
}
|
||||
if(i != alen - 1) {
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), ",");
|
||||
}
|
||||
}
|
||||
b += c_snprintf(b, BUF_LEFT(size, b - buf), "]");
|
||||
mjs->json_visited_stack.len -= sizeof(v);
|
||||
len = b - buf;
|
||||
goto clean;
|
||||
}
|
||||
|
||||
case MJS_TYPES_CNT:
|
||||
abort();
|
||||
}
|
||||
|
||||
abort();
|
||||
|
||||
len = 0; /* for compilers that don't know about abort() */
|
||||
goto clean;
|
||||
|
||||
clean:
|
||||
if(rcode != MJS_OK) {
|
||||
len = 0;
|
||||
}
|
||||
if(res_len != NULL) {
|
||||
*res_len = len;
|
||||
}
|
||||
return rcode;
|
||||
}
|
||||
|
||||
MJS_PRIVATE mjs_err_t
|
||||
mjs_json_stringify(struct mjs* mjs, mjs_val_t v, char* buf, size_t size, char** res) {
|
||||
mjs_err_t rcode = MJS_OK;
|
||||
char* p = buf;
|
||||
size_t len;
|
||||
|
||||
to_json_or_debug(mjs, v, buf, size, &len, 0);
|
||||
|
||||
if(len >= size) {
|
||||
/* Buffer is not large enough. Allocate a bigger one */
|
||||
p = (char*)malloc(len + 1);
|
||||
rcode = mjs_json_stringify(mjs, v, p, len + 1, res);
|
||||
assert(*res == p);
|
||||
goto clean;
|
||||
} else {
|
||||
*res = p;
|
||||
goto clean;
|
||||
}
|
||||
|
||||
clean:
|
||||
/*
|
||||
* If we're going to return an error, and we allocated a buffer, then free
|
||||
* it. Otherwise, caller should free it.
|
||||
*/
|
||||
if(rcode != MJS_OK && p != buf) {
|
||||
free(p);
|
||||
}
|
||||
return rcode;
|
||||
}
|
||||
|
||||
/*
|
||||
* JSON parsing frame: a separate frame is allocated for each nested
|
||||
* object/array during parsing
|
||||
*/
|
||||
struct json_parse_frame {
|
||||
mjs_val_t val;
|
||||
struct json_parse_frame* up;
|
||||
};
|
||||
|
||||
/*
|
||||
* Context for JSON parsing by means of json_walk()
|
||||
*/
|
||||
struct json_parse_ctx {
|
||||
struct mjs* mjs;
|
||||
mjs_val_t result;
|
||||
struct json_parse_frame* frame;
|
||||
enum mjs_err rcode;
|
||||
};
|
||||
|
||||
/* Allocate JSON parse frame */
|
||||
static struct json_parse_frame* alloc_json_frame(struct json_parse_ctx* ctx, mjs_val_t v) {
|
||||
struct json_parse_frame* frame =
|
||||
(struct json_parse_frame*)calloc(sizeof(struct json_parse_frame), 1);
|
||||
frame->val = v;
|
||||
mjs_own(ctx->mjs, &frame->val);
|
||||
return frame;
|
||||
}
|
||||
|
||||
/* Free JSON parse frame, return the previous one (which may be NULL) */
|
||||
static struct json_parse_frame*
|
||||
free_json_frame(struct json_parse_ctx* ctx, struct json_parse_frame* frame) {
|
||||
struct json_parse_frame* up = frame->up;
|
||||
mjs_disown(ctx->mjs, &frame->val);
|
||||
free(frame);
|
||||
return up;
|
||||
}
|
||||
|
||||
/* Callback for json_walk() */
|
||||
static void frozen_cb(
|
||||
void* data,
|
||||
const char* name,
|
||||
size_t name_len,
|
||||
const char* path,
|
||||
const struct json_token* token) {
|
||||
struct json_parse_ctx* ctx = (struct json_parse_ctx*)data;
|
||||
mjs_val_t v = MJS_UNDEFINED;
|
||||
|
||||
(void)path;
|
||||
|
||||
mjs_own(ctx->mjs, &v);
|
||||
|
||||
switch(token->type) {
|
||||
case JSON_TYPE_STRING: {
|
||||
char* dst;
|
||||
if(token->len > 0 && (dst = malloc(token->len)) != NULL) {
|
||||
int len = json_unescape(token->ptr, token->len, dst, token->len);
|
||||
if(len < 0) {
|
||||
mjs_prepend_errorf(ctx->mjs, MJS_TYPE_ERROR, "invalid JSON string");
|
||||
break;
|
||||
}
|
||||
v = mjs_mk_string(ctx->mjs, dst, len, 1 /* copy */);
|
||||
free(dst);
|
||||
} else {
|
||||
/*
|
||||
* This branch is for 0-len strings, and for malloc errors
|
||||
* TODO(lsm): on malloc error, propagate the error upstream
|
||||
*/
|
||||
v = mjs_mk_string(ctx->mjs, "", 0, 1 /* copy */);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case JSON_TYPE_NUMBER:
|
||||
v = mjs_mk_number(ctx->mjs, strtod(token->ptr, NULL));
|
||||
break;
|
||||
case JSON_TYPE_TRUE:
|
||||
v = mjs_mk_boolean(ctx->mjs, 1);
|
||||
break;
|
||||
case JSON_TYPE_FALSE:
|
||||
v = mjs_mk_boolean(ctx->mjs, 0);
|
||||
break;
|
||||
case JSON_TYPE_NULL:
|
||||
v = MJS_NULL;
|
||||
break;
|
||||
case JSON_TYPE_OBJECT_START:
|
||||
v = mjs_mk_object(ctx->mjs);
|
||||
break;
|
||||
case JSON_TYPE_ARRAY_START:
|
||||
v = mjs_mk_array(ctx->mjs);
|
||||
break;
|
||||
|
||||
case JSON_TYPE_OBJECT_END:
|
||||
case JSON_TYPE_ARRAY_END: {
|
||||
/* Object or array has finished: deallocate its frame */
|
||||
ctx->frame = free_json_frame(ctx, ctx->frame);
|
||||
} break;
|
||||
|
||||
default:
|
||||
LOG(LL_ERROR, ("Wrong token type %d\n", token->type));
|
||||
break;
|
||||
}
|
||||
|
||||
if(!mjs_is_undefined(v)) {
|
||||
if(name != NULL && name_len != 0) {
|
||||
/* Need to define a property on the current object/array */
|
||||
if(mjs_is_object(ctx->frame->val)) {
|
||||
mjs_set(ctx->mjs, ctx->frame->val, name, name_len, v);
|
||||
} else if(mjs_is_array(ctx->frame->val)) {
|
||||
/*
|
||||
* TODO(dfrank): consult name_len. Currently it's not a problem due to
|
||||
* the implementation details of frozen, but it might change
|
||||
*/
|
||||
int idx = (int)strtod(name, NULL);
|
||||
mjs_array_set(ctx->mjs, ctx->frame->val, idx, v);
|
||||
} else {
|
||||
LOG(LL_ERROR, ("Current value is neither object nor array\n"));
|
||||
}
|
||||
} else {
|
||||
/* This is a root value */
|
||||
assert(ctx->frame == NULL);
|
||||
|
||||
/*
|
||||
* This value will also be the overall result of JSON parsing
|
||||
* (it's already owned by the `mjs_alt_json_parse()`)
|
||||
*/
|
||||
ctx->result = v;
|
||||
}
|
||||
|
||||
if(token->type == JSON_TYPE_OBJECT_START || token->type == JSON_TYPE_ARRAY_START) {
|
||||
/* New object or array has just started, so we need to allocate a frame
|
||||
* for it */
|
||||
struct json_parse_frame* new_frame = alloc_json_frame(ctx, v);
|
||||
new_frame->up = ctx->frame;
|
||||
ctx->frame = new_frame;
|
||||
}
|
||||
}
|
||||
|
||||
mjs_disown(ctx->mjs, &v);
|
||||
}
|
||||
|
||||
MJS_PRIVATE mjs_err_t mjs_json_parse(struct mjs* mjs, const char* str, size_t len, mjs_val_t* res) {
|
||||
struct json_parse_ctx* ctx = (struct json_parse_ctx*)calloc(sizeof(struct json_parse_ctx), 1);
|
||||
int json_res;
|
||||
enum mjs_err rcode = MJS_OK;
|
||||
|
||||
ctx->mjs = mjs;
|
||||
ctx->result = MJS_UNDEFINED;
|
||||
ctx->frame = NULL;
|
||||
ctx->rcode = MJS_OK;
|
||||
|
||||
mjs_own(mjs, &ctx->result);
|
||||
|
||||
{
|
||||
/*
|
||||
* We have to reallocate the buffer before invoking json_walk, because
|
||||
* frozen_cb can create new strings, which can result in the reallocation
|
||||
* of mjs string mbuf, invalidating the `str` pointer.
|
||||
*/
|
||||
char* stmp = malloc(len);
|
||||
memcpy(stmp, str, len);
|
||||
json_res = json_walk(stmp, len, frozen_cb, ctx);
|
||||
free(stmp);
|
||||
stmp = NULL;
|
||||
|
||||
/* str might have been invalidated, so null it out */
|
||||
str = NULL;
|
||||
}
|
||||
|
||||
if(ctx->rcode != MJS_OK) {
|
||||
rcode = ctx->rcode;
|
||||
mjs_prepend_errorf(mjs, rcode, "invalid JSON string");
|
||||
} else if(json_res < 0) {
|
||||
/* There was an error during parsing */
|
||||
rcode = MJS_TYPE_ERROR;
|
||||
mjs_prepend_errorf(mjs, rcode, "invalid JSON string");
|
||||
} else {
|
||||
/* Expression is parsed successfully */
|
||||
*res = ctx->result;
|
||||
|
||||
/* There should be no allocated frames */
|
||||
assert(ctx->frame == NULL);
|
||||
}
|
||||
|
||||
if(rcode != MJS_OK) {
|
||||
/* There might be some allocated frames in case of malformed JSON */
|
||||
while(ctx->frame != NULL) {
|
||||
ctx->frame = free_json_frame(ctx, ctx->frame);
|
||||
}
|
||||
}
|
||||
|
||||
mjs_disown(mjs, &ctx->result);
|
||||
free(ctx);
|
||||
|
||||
return rcode;
|
||||
}
|
||||
|
||||
MJS_PRIVATE void mjs_op_json_stringify(struct mjs* mjs) {
|
||||
mjs_val_t ret = MJS_UNDEFINED;
|
||||
mjs_val_t val = mjs_arg(mjs, 0);
|
||||
|
||||
if(mjs_nargs(mjs) < 1) {
|
||||
mjs_prepend_errorf(mjs, MJS_TYPE_ERROR, "missing a value to stringify");
|
||||
} else {
|
||||
char* p = NULL;
|
||||
if(mjs_json_stringify(mjs, val, NULL, 0, &p) == MJS_OK) {
|
||||
ret = mjs_mk_string(mjs, p, ~0, 1 /* copy */);
|
||||
free(p);
|
||||
}
|
||||
}
|
||||
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
|
||||
MJS_PRIVATE void mjs_op_json_parse(struct mjs* mjs) {
|
||||
mjs_val_t ret = MJS_UNDEFINED;
|
||||
mjs_val_t arg0 = mjs_arg(mjs, 0);
|
||||
|
||||
if(mjs_is_string(arg0)) {
|
||||
size_t len;
|
||||
const char* str = mjs_get_string(mjs, &arg0, &len);
|
||||
mjs_json_parse(mjs, str, len, &ret);
|
||||
} else {
|
||||
mjs_prepend_errorf(mjs, MJS_TYPE_ERROR, "string argument required");
|
||||
}
|
||||
|
||||
mjs_return(mjs, ret);
|
||||
}
|
||||
Reference in New Issue
Block a user