diff --git a/applications/debug/unit_tests/tests/js/js_test.c b/applications/debug/unit_tests/tests/js/js_test.c index af590e8995..eb4ddf865f 100644 --- a/applications/debug/unit_tests/tests/js/js_test.c +++ b/applications/debug/unit_tests/tests/js/js_test.c @@ -6,9 +6,12 @@ #include #include +#include #include +#define TAG "JsUnitTests" + #define JS_SCRIPT_PATH(name) EXT_PATH("unit_tests/js/" name ".js") typedef enum { @@ -73,7 +76,268 @@ MU_TEST(js_test_storage) { js_test_run(JS_SCRIPT_PATH("storage")); } +static void js_value_test_compatibility_matrix(struct mjs* mjs) { + static const JsValueType types[] = { + JsValueTypeAny, + JsValueTypeAnyArray, + JsValueTypeAnyObject, + JsValueTypeFunction, + JsValueTypeRawPointer, + JsValueTypeInt32, + JsValueTypeDouble, + JsValueTypeString, + JsValueTypeBool, + }; + + mjs_val_t values[] = { + mjs_mk_undefined(), + mjs_mk_foreign(mjs, (void*)0xDEADBEEF), + mjs_mk_array(mjs), + mjs_mk_object(mjs), + mjs_mk_number(mjs, 123.456), + mjs_mk_string(mjs, "test", ~0, false), + mjs_mk_boolean(mjs, true), + }; + +// for proper matrix formatting and better readability +#define YES true +#define NO_ false + static const int success_matrix[COUNT_OF(types)][COUNT_OF(values)] = { + // types: + {YES, YES, YES, YES, YES, YES, YES}, // any + {NO_, NO_, YES, NO_, NO_, NO_, NO_}, // array + {NO_, NO_, YES, YES, NO_, NO_, NO_}, // obj + {NO_, NO_, NO_, NO_, NO_, NO_, NO_}, // fn + {NO_, YES, NO_, NO_, NO_, NO_, NO_}, // ptr + {NO_, NO_, NO_, NO_, YES, NO_, NO_}, // int32 + {NO_, NO_, NO_, NO_, YES, NO_, NO_}, // double + {NO_, NO_, NO_, NO_, NO_, YES, NO_}, // str + {NO_, NO_, NO_, NO_, NO_, NO_, YES}, // bool + //und ptr arr obj num str bool <- values + }; +#undef NO_ +#undef YES + + for(size_t i = 0; i < COUNT_OF(types); i++) { + for(size_t j = 0; j < COUNT_OF(values); j++) { + const JsValueDeclaration declaration = { + .type = types[i], + .permit_null = false, + .n_children = 0, + }; + // we only care about the status, not the result. double has the largest size out of + // all the results + uint8_t result[sizeof(double)]; + JsValueParseStatus status; + JS_VALUE_PARSE(mjs, &declaration, JsValueParseFlagNone, &status, &values[j], result); + if((status == JsValueParseStatusOk) != success_matrix[i][j]) { + FURI_LOG_E(TAG, "type %zu, value %zu", i, j); + mu_fail("see serial logs"); + } + } + } +} + +static void js_value_test_literal(struct mjs* mjs) { + static const JsValueType types[] = { + JsValueTypeAny, + JsValueTypeAnyArray, + JsValueTypeAnyObject, + }; + + mjs_val_t values[] = { + mjs_mk_undefined(), + mjs_mk_array(mjs), + mjs_mk_object(mjs), + }; + + mu_assert_int_eq(COUNT_OF(types), COUNT_OF(values)); + for(size_t i = 0; i < COUNT_OF(types); i++) { + const JsValueDeclaration declaration = { + .type = types[i], + .permit_null = false, + .n_children = 0, + }; + mjs_val_t result; + JsValueParseStatus status; + JS_VALUE_PARSE(mjs, &declaration, JsValueParseFlagNone, &status, &values[i], &result); + mu_assert_int_eq(JsValueParseStatusOk, status); + mu_assert(result == values[i], "wrong result"); + } +} + +static void js_value_test_primitive( + struct mjs* mjs, + JsValueType type, + const void* c_value, + size_t c_value_size, + mjs_val_t js_val) { + const JsValueDeclaration declaration = { + .type = type, + .permit_null = false, + .n_children = 0, + }; + uint8_t result[c_value_size]; + JsValueParseStatus status; + JS_VALUE_PARSE(mjs, &declaration, JsValueParseFlagNone, &status, &js_val, result); + mu_assert_int_eq(JsValueParseStatusOk, status); + if(type == JsValueTypeString) { + const char* result_str = *(const char**)&result; + mu_assert_string_eq(c_value, result_str); + } else { + mu_assert_mem_eq(c_value, result, c_value_size); + } +} + +static void js_value_test_primitives(struct mjs* mjs) { + int32_t i32 = 123; + js_value_test_primitive(mjs, JsValueTypeInt32, &i32, sizeof(i32), mjs_mk_number(mjs, i32)); + + double dbl = 123.456; + js_value_test_primitive(mjs, JsValueTypeDouble, &dbl, sizeof(dbl), mjs_mk_number(mjs, dbl)); + + const char* str = "test"; + js_value_test_primitive( + mjs, JsValueTypeString, str, strlen(str) + 1, mjs_mk_string(mjs, str, ~0, false)); + + bool boolean = true; + js_value_test_primitive( + mjs, JsValueTypeBool, &boolean, sizeof(boolean), mjs_mk_boolean(mjs, boolean)); +} + +static uint32_t + js_value_test_enum(struct mjs* mjs, const JsValueDeclaration* decl, const char* value) { + mjs_val_t str = mjs_mk_string(mjs, value, ~0, false); + uint32_t result; + furi_check(decl->enum_size == sizeof(result)); + JsValueParseStatus status; + JS_VALUE_PARSE(mjs, decl, JsValueParseFlagNone, &status, &str, &result); + if(status != JsValueParseStatusOk) return 0; + return result; +} + +static void js_value_test_enums(struct mjs* mjs) { + static const JsValueDeclaration enum_1_variants[] = { + JS_VALUE_ENUM_VARIANT("variant 1", 1), + JS_VALUE_ENUM_VARIANT("variant 2", 2), + JS_VALUE_ENUM_VARIANT("variant 3", 3), + }; + static const JsValueDeclaration enum_1 = { + .type = JsValueTypeEnum, + .enum_size = sizeof(uint32_t), + JS_VALUE_CHILDREN(enum_1_variants), + }; + + static const JsValueDeclaration enum_2_variants[] = { + JS_VALUE_ENUM_VARIANT("read", 4), + JS_VALUE_ENUM_VARIANT("write", 8), + }; + static const JsValueDeclaration enum_2 = { + .type = JsValueTypeEnum, + .enum_size = sizeof(uint32_t), + JS_VALUE_CHILDREN(enum_2_variants), + }; + + mu_assert_int_eq(1, js_value_test_enum(mjs, &enum_1, "variant 1")); + mu_assert_int_eq(2, js_value_test_enum(mjs, &enum_1, "variant 2")); + mu_assert_int_eq(3, js_value_test_enum(mjs, &enum_1, "variant 3")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_1, "not a thing")); + + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 1")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 2")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "variant 3")); + mu_assert_int_eq(0, js_value_test_enum(mjs, &enum_2, "not a thing")); + mu_assert_int_eq(4, js_value_test_enum(mjs, &enum_2, "read")); + mu_assert_int_eq(8, js_value_test_enum(mjs, &enum_2, "write")); +} + +static void js_value_test_object(struct mjs* mjs) { + static const JsValueDeclaration enum_variants[] = { + JS_VALUE_ENUM_VARIANT("variant 1", 1), + JS_VALUE_ENUM_VARIANT("variant 2", 2), + JS_VALUE_ENUM_VARIANT("variant 3", 3), + }; + static const JsValueDeclaration fields[] = { + {.type = JsValueTypeInt32, .object_field_name = "int"}, + {.type = JsValueTypeString, .object_field_name = "str"}, + {.type = JsValueTypeEnum, + .object_field_name = "enum", + .enum_size = sizeof(uint32_t), + JS_VALUE_CHILDREN(enum_variants)}}; + static const JsValueDeclaration object_decl = { + .type = JsValueTypeObject, + JS_VALUE_CHILDREN(fields), + }; + + mjs_val_t object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, object) { + JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false)); + JS_FIELD("int", mjs_mk_number(mjs, 123)); + JS_FIELD("enum", mjs_mk_string(mjs, "variant 2", ~0, false)); + } + + const char* result_str; + int32_t result_int; + uint32_t result_enum; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, + &object_decl, + JsValueParseFlagNone, + &status, + &object, + &result_int, + &result_str, + &result_enum); + mu_assert_int_eq(JsValueParseStatusOk, status); + mu_assert_string_eq("Helloooo!", result_str); + mu_assert_int_eq(123, result_int); + mu_assert_int_eq(2, result_enum); +} + +static void js_value_test_default(struct mjs* mjs) { + static const JsValueDeclaration fields[] = { + {.type = JsValueTypeInt32, + .permit_null = true, + .default_value = {.int32_val = 123}, + .object_field_name = "int"}, + {.type = JsValueTypeString, .object_field_name = "str"}, + }; + static const JsValueDeclaration object_decl = { + .type = JsValueTypeObject, + JS_VALUE_CHILDREN(fields), + }; + + mjs_val_t object = mjs_mk_object(mjs); + JS_ASSIGN_MULTI(mjs, object) { + JS_FIELD("str", mjs_mk_string(mjs, "Helloooo!", ~0, false)); + JS_FIELD("int", mjs_mk_undefined()); + } + + const char* result_str; + int32_t result_int; + JsValueParseStatus status; + JS_VALUE_PARSE( + mjs, &object_decl, JsValueParseFlagNone, &status, &object, &result_int, &result_str); + mu_assert_string_eq("Helloooo!", result_str); + mu_assert_int_eq(123, result_int); +} + +MU_TEST(js_value_test) { + struct mjs* mjs = mjs_create(NULL); + + js_value_test_compatibility_matrix(mjs); + js_value_test_literal(mjs); + js_value_test_primitives(mjs); + js_value_test_enums(mjs); + js_value_test_object(mjs); + js_value_test_default(mjs); + + mjs_destroy(mjs); +} + MU_TEST_SUITE(test_js) { + MU_RUN_TEST(js_value_test); MU_RUN_TEST(js_test_basic); MU_RUN_TEST(js_test_math); MU_RUN_TEST(js_test_event_loop); diff --git a/applications/debug/unit_tests/unit_test_api_table_i.h b/applications/debug/unit_tests/unit_test_api_table_i.h index 10b0890225..c682c56279 100644 --- a/applications/debug/unit_tests/unit_test_api_table_i.h +++ b/applications/debug/unit_tests/unit_test_api_table_i.h @@ -8,6 +8,7 @@ #include #include #include +#include static constexpr auto unit_tests_api_table = sort(create_array_t( API_METHOD(resource_manifest_reader_alloc, ResourceManifestReader*, (Storage*)), @@ -38,4 +39,16 @@ static constexpr auto unit_tests_api_table = sort(create_array_t( JsThread*, (const char* script_path, JsThreadCallback callback, void* context)), API_METHOD(js_thread_stop, void, (JsThread * worker)), + API_METHOD(js_value_buffer_size, size_t, (const JsValueDeclaration* declaration)), + API_METHOD( + js_value_parse, + JsValueParseStatus, + (struct mjs * mjs, + const JsValueDeclaration* declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...)), API_VARIABLE(PB_Main_msg, PB_Main_msg_t))); diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index db1521b9d8..66ec221ec7 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -11,6 +11,7 @@ App( "js_app.c", "js_modules.c", "js_thread.c", + "js_value.c", "plugin_api/app_api_table.cpp", "views/console_view.c", "modules/js_flipper.c", diff --git a/applications/system/js_app/js_modules.h b/applications/system/js_app/js_modules.h index 2babe231ef..130ee975c1 100644 --- a/applications/system/js_app/js_modules.h +++ b/applications/system/js_app/js_modules.h @@ -2,6 +2,7 @@ #include #include "js_thread_i.h" +#include "js_value.h" #include #include #include diff --git a/applications/system/js_app/js_value.c b/applications/system/js_app/js_value.c new file mode 100644 index 0000000000..75304952f3 --- /dev/null +++ b/applications/system/js_app/js_value.c @@ -0,0 +1,274 @@ +#include "js_value.h" +#include + +size_t js_value_buffer_size(const JsValueDeclaration* declaration) { + JsValueType type = declaration->type; + + if(type == JsValueTypeString) return 1; + + if(type == JsValueTypeObject) { + size_t total = 0; + for(size_t i = 0; i < declaration->n_children; i++) { + total += js_value_buffer_size(&declaration->children[i]); + } + return total; + } + + return 0; +} + +static size_t js_value_resulting_c_values_count(const JsValueDeclaration* declaration) { + JsValueType type = declaration->type; + + if(type == JsValueTypeObject) { + size_t total = 0; + for(size_t i = 0; i < declaration->n_children; i++) { + total += js_value_resulting_c_values_count(&declaration->children[i]); + } + return total; + } + + return 1; +} + +static bool js_value_declaration_valid(const JsValueDeclaration* declaration) { + JsValueType type = declaration->type; + + // Args can have an arbitrary number of children of arbitrary types + if(type == JsValueTypeArgs) { + for(size_t i = 0; i < declaration->n_children; i++) + if(!js_value_declaration_valid(&declaration->children[i])) return false; + if(declaration->permit_null) return false; + return true; + } + + // Enums can only have EnumValue children + if(type == JsValueTypeEnum) { + if(declaration->enum_size != 1 && declaration->enum_size != 2 && + declaration->enum_size != 4) + return false; + for(size_t i = 0; i < declaration->n_children; i++) { + const JsValueDeclaration* child = &declaration->children[i]; + if(!js_value_declaration_valid(child) || child->type != JsValueTypeEnumValue) + return false; + } + return true; + } + + // Objects must have valid children + if(type == JsValueTypeObject) { + for(size_t i = 0; i < declaration->n_children; i++) { + const JsValueDeclaration* child = &declaration->children[i]; + if(!js_value_declaration_valid(child) || !child->object_field_name) return false; + } + if(declaration->permit_null) return false; + return true; + } + + // EnumValues must have their string field set + if(type == JsValueTypeEnumValue) { + return declaration->n_children == 0 && declaration->enum_string_value != NULL; + } + + // Literal types can't have default values + if(type == JsValueTypeAny || type == JsValueTypeAnyArray || type == JsValueTypeAnyObject || + type == JsValueTypeFunction) { + if(declaration->permit_null) return false; + } + + // All other types can't have children + return declaration->n_children == 0; +} + +#define PREPEND_JS_ERROR_AND_RETURN(mjs, flags, ...) \ + do { \ + if((flags) & JsValueParseFlagReturnOnError) \ + mjs_prepend_errorf((mjs), MJS_BAD_ARGS_ERROR, __VA_ARGS__); \ + return JsValueParseStatusJsError; \ + } while(0) + +static void js_value_assign_enum_val(va_list* out_pointers, size_t enum_size, uint32_t value) { + if(enum_size == 1) + *va_arg(*out_pointers, uint8_t*) = value; + else if(enum_size == 2) + *va_arg(*out_pointers, uint16_t*) = value; + else if(enum_size == 4) + *va_arg(*out_pointers, uint32_t*) = value; +} + +static bool js_value_is_null_or_undefined(mjs_val_t* val_ptr) { + return mjs_is_null(*val_ptr) || mjs_is_undefined(*val_ptr); +} + +static bool js_value_maybe_assign_default( + const JsValueDeclaration* declaration, + mjs_val_t* val_ptr, + void* destination, + size_t size) { + if(declaration->permit_null && js_value_is_null_or_undefined(val_ptr)) { + memcpy(destination, &declaration->default_value, size); + return true; + } + return false; +} + +static JsValueParseStatus js_value_parse_va( + struct mjs* mjs, + const JsValueDeclaration* declaration, + JsValueParseFlag flags, + mjs_val_t* source, + mjs_val_t* buffer, + size_t* buffer_index, + va_list* out_pointers) { + switch(declaration->type) { + // Literal terms + case JsValueTypeAny: { + *va_arg(*out_pointers, mjs_val_t*) = *source; + break; + } + case JsValueTypeAnyArray: { + if(!mjs_is_array(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected array"); + *va_arg(*out_pointers, mjs_val_t*) = *source; + break; + } + case JsValueTypeAnyObject: { + if(!mjs_is_object(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected object"); + *va_arg(*out_pointers, mjs_val_t*) = *source; + break; + } + case JsValueTypeFunction: { + if(!mjs_is_function(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected function"); + *va_arg(*out_pointers, mjs_val_t*) = *source; + break; + } + + // Primitive types + case JsValueTypeRawPointer: { + void** destination = *va_arg(*out_pointers, void**); + if(js_value_maybe_assign_default(declaration, source, destination, sizeof(void*))) break; + if(!mjs_is_foreign(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected pointer"); + *destination = mjs_get_ptr(mjs, *source); + break; + } + case JsValueTypeInt32: { + int32_t* destination = va_arg(*out_pointers, int32_t*); + if(js_value_maybe_assign_default(declaration, source, destination, sizeof(int32_t))) break; + if(!mjs_is_number(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected number"); + *destination = mjs_get_int32(mjs, *source); + break; + } + case JsValueTypeDouble: { + double* destination = va_arg(*out_pointers, double*); + if(js_value_maybe_assign_default(declaration, source, destination, sizeof(double))) break; + if(!mjs_is_number(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected number"); + *destination = mjs_get_double(mjs, *source); + break; + } + case JsValueTypeBool: { + bool* destination = va_arg(*out_pointers, bool*); + if(js_value_maybe_assign_default(declaration, source, destination, sizeof(bool))) break; + if(!mjs_is_boolean(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected bool"); + *destination = mjs_get_bool(mjs, *source); + break; + } + case JsValueTypeString: { + const char** destination = va_arg(*out_pointers, const char**); + if(js_value_maybe_assign_default(declaration, source, destination, sizeof(const char*))) + break; + if(!mjs_is_string(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected string"); + buffer[*buffer_index] = *source; + *destination = mjs_get_string(mjs, &buffer[*buffer_index], NULL); + (*buffer_index)++; + break; + } + + // Types with children + case JsValueTypeArgs: { + furi_check(source == JS_VAL_PARSE_SOURCE_ARGS); + size_t args_provided = mjs_nargs(mjs); + for(size_t i = 0; i < declaration->n_children; i++) { + mjs_val_t arg = (i < args_provided) ? mjs_arg(mjs, i) : MJS_UNDEFINED; + JsValueParseStatus status = js_value_parse_va( + mjs, declaration, flags, &arg, buffer, buffer_index, out_pointers); + if(status != JsValueParseStatusOk) + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "argument %zu: ", i); + } + break; + } + case JsValueTypeEnum: { + if(declaration->permit_null && js_value_is_null_or_undefined(source)) { + js_value_assign_enum_val( + out_pointers, declaration->enum_size, declaration->default_value.enum_val); + } else if(mjs_is_string(*source)) { + const char* str = mjs_get_string(mjs, source, NULL); + furi_check(str); + bool match_found = false; + for(size_t i = 0; i < declaration->n_children; i++) { + const JsValueDeclaration* child = &declaration->children[i]; + furi_check(child->type == JsValueTypeEnumValue); + if(strcmp(str, child->enum_string_value) == 0) { + js_value_assign_enum_val( + out_pointers, declaration->enum_size, child->enum_value); + match_found = true; + break; + } + } + if(!match_found) + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected one of permitted strings"); + } else { + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected string"); + } + break; + } + + case JsValueTypeObject: { + if(!mjs_is_object(*source)) PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "expected object"); + for(size_t i = 0; i < declaration->n_children; i++) { + const JsValueDeclaration* child = &declaration->children[i]; + mjs_val_t field = mjs_get(mjs, *source, child->object_field_name, ~0); + JsValueParseStatus status = + js_value_parse_va(mjs, child, flags, &field, buffer, buffer_index, out_pointers); + if(status != JsValueParseStatusOk) + PREPEND_JS_ERROR_AND_RETURN(mjs, flags, "field %s: ", child->object_field_name); + } + break; + } + + case JsValueTypeEnumValue: + furi_crash(); + } + + return JsValueParseStatusOk; +} + +JsValueParseStatus js_value_parse( + struct mjs* mjs, + const JsValueDeclaration* declaration, + JsValueParseFlag flags, + mjs_val_t* buffer, + size_t buf_size, + mjs_val_t* source, + size_t n_c_vals, + ...) { + furi_check(mjs); + furi_check(declaration); + furi_check(buffer); + + // These are asserts and not checks because argument parsing has to be fast. + // People bitbang I2C from JS. + furi_assert(js_value_declaration_valid(declaration)); + furi_assert(buf_size == js_value_buffer_size(declaration)); + furi_assert(n_c_vals == js_value_resulting_c_values_count(declaration)); + + va_list out_pointers; + va_start(out_pointers, n_c_vals); + + size_t buffer_index = 0; + JsValueParseStatus status = + js_value_parse_va(mjs, declaration, flags, source, buffer, &buffer_index, &out_pointers); + furi_check(buffer_index <= buf_size); + + va_end(out_pointers); + + return status; +} diff --git a/applications/system/js_app/js_value.h b/applications/system/js_app/js_value.h new file mode 100644 index 0000000000..d5509d04e7 --- /dev/null +++ b/applications/system/js_app/js_value.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include "js_modules.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + JsValueTypeArgs, //