mirror of
https://github.com/DarkFlippers/unleashed-firmware.git
synced 2024-12-25 22:32:29 +03:00
0154018363
* 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>
524 lines
15 KiB
C
524 lines
15 KiB
C
/*
|
|
* 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);
|
|
}
|