@mikrojs/native 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CMakeLists.txt +62 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/result/native-result.node-shim.d.ts +3 -0
- package/dist/runtime/result/native-result.node-shim.d.ts.map +1 -0
- package/dist/runtime/result/native-result.node-shim.js +41 -0
- package/dist/runtime/result/native-result.node-shim.js.map +1 -0
- package/dist/runtime/result/types.d.ts +55 -0
- package/dist/runtime/result/types.d.ts.map +1 -0
- package/dist/runtime/result/types.js +2 -0
- package/dist/runtime/result/types.js.map +1 -0
- package/dist/runtime/schema/core.d.ts +115 -0
- package/dist/runtime/schema/core.d.ts.map +1 -0
- package/dist/runtime/schema/core.js +259 -0
- package/dist/runtime/schema/core.js.map +1 -0
- package/dist/runtime/schema/shared.d.ts +54 -0
- package/dist/runtime/schema/shared.d.ts.map +1 -0
- package/dist/runtime/schema/shared.js +489 -0
- package/dist/runtime/schema/shared.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/include/mikrojs/cbor_helpers.h +20 -0
- package/include/mikrojs/mem.h +11 -0
- package/include/mikrojs/mikrojs.h +2 -1
- package/include/mikrojs/ota_client.h +342 -0
- package/include/mikrojs/ota_config.h +100 -0
- package/include/mikrojs/ota_env.h +192 -0
- package/include/mikrojs/ota_js_hooks.h +71 -0
- package/include/mikrojs/ota_policy.h +131 -0
- package/include/mikrojs/ota_slots.h +47 -0
- package/include/mikrojs/sys_codec.h +61 -0
- package/package.json +7 -5
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/internal.d.ts +22 -16
- package/runtime/kv/shared.ts +11 -5
- package/runtime/kv/types.ts +4 -4
- package/runtime/ota/client.ts +12 -51
- package/runtime/ota/config.ts +18 -0
- package/runtime/ota/ota.ts +28 -70
- package/runtime/ota/types.ts +220 -2
- package/runtime/schema/core.ts +539 -0
- package/runtime/schema/schema.ts +36 -314
- package/runtime/schema/shared.ts +494 -0
- package/runtime/schema/types.ts +84 -12
- package/scripts/bundle-runtime.js +33 -0
- package/scripts/gen-checkin-fixtures.js +323 -0
- package/src/builtins.cpp +7 -8
- package/src/fs.cpp +3 -0
- package/src/mem.cpp +38 -0
- package/src/mik_abort.cpp +8 -1
- package/src/mik_cbor.cpp +43 -5
- package/src/mik_inspect.cpp +128 -22
- package/src/mik_ota_client.cpp +1230 -0
- package/src/mik_ota_config.cpp +296 -0
- package/src/mik_ota_js_hooks.cpp +190 -0
- package/src/mik_ota_policy.cpp +419 -0
- package/src/mik_ota_slots.cpp +249 -0
- package/src/mik_repl.cpp +9 -3
- package/src/mik_result.cpp +3 -1
- package/src/mik_sys_codec.cpp +167 -0
- package/src/mikrojs.cpp +15 -0
- package/src/modules.cpp +32 -13
- package/runtime/ota/client-impl.ts +0 -590
- package/runtime/ota/policy.ts +0 -299
package/src/mik_result.cpp
CHANGED
|
@@ -86,7 +86,9 @@ JSValue mik__result_err(JSContext* ctx, int code, int platform_errno, const char
|
|
|
86
86
|
JSValue mik__result_err_named(JSContext* ctx, const char* name, const char* fmt, ...) {
|
|
87
87
|
va_list ap;
|
|
88
88
|
va_start(ap, fmt);
|
|
89
|
-
|
|
89
|
+
/* 384 matches the HTTP task's error_buf: fetch errors pass through here
|
|
90
|
+
* verbatim, and a smaller buffer truncates their trailing heap figures. */
|
|
91
|
+
char msg[384];
|
|
90
92
|
vsnprintf(msg, sizeof(msg), fmt, ap);
|
|
91
93
|
va_end(ap);
|
|
92
94
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#include "mikrojs/sys_codec.h"
|
|
2
|
+
|
|
3
|
+
#include <nanocbor/nanocbor.h>
|
|
4
|
+
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstdio>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
|
|
9
|
+
size_t mik__kv_encode_str(const char* value, uint8_t* out, size_t out_len) {
|
|
10
|
+
/* The measuring pass needs a non-null base: an empty string still reaches
|
|
11
|
+
* memcpy with length 0, and memcpy(NULL, ..., 0) is undefined behavior. */
|
|
12
|
+
static uint8_t measure_base;
|
|
13
|
+
nanocbor_encoder_t enc;
|
|
14
|
+
nanocbor_encoder_init(&enc, &measure_base, 0);
|
|
15
|
+
nanocbor_put_tstr(&enc, value ? value : "");
|
|
16
|
+
size_t needed = nanocbor_encoded_len(&enc);
|
|
17
|
+
if (!out || out_len < needed) return needed;
|
|
18
|
+
nanocbor_encoder_init(&enc, out, out_len);
|
|
19
|
+
nanocbor_put_tstr(&enc, value ? value : "");
|
|
20
|
+
return needed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
bool mik__kv_decode_str(const uint8_t* in, size_t in_len, char* out, size_t out_len) {
|
|
24
|
+
if (!in || in_len == 0 || !out || out_len == 0) return false;
|
|
25
|
+
nanocbor_value_t it;
|
|
26
|
+
nanocbor_decoder_init(&it, in, in_len);
|
|
27
|
+
const uint8_t* ptr = nullptr;
|
|
28
|
+
size_t len = 0;
|
|
29
|
+
if (nanocbor_get_tstr(&it, &ptr, &len) < 0) return false;
|
|
30
|
+
if (len + 1 > out_len) return false;
|
|
31
|
+
memcpy(out, ptr, len);
|
|
32
|
+
out[len] = '\0';
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
size_t mik__kv_encode_i32(int32_t value, uint8_t* out, size_t out_len) {
|
|
37
|
+
static uint8_t measure_base;
|
|
38
|
+
nanocbor_encoder_t enc;
|
|
39
|
+
nanocbor_encoder_init(&enc, &measure_base, 0);
|
|
40
|
+
nanocbor_fmt_int(&enc, value);
|
|
41
|
+
size_t needed = nanocbor_encoded_len(&enc);
|
|
42
|
+
if (!out || out_len < needed) return needed;
|
|
43
|
+
nanocbor_encoder_init(&enc, out, out_len);
|
|
44
|
+
nanocbor_fmt_int(&enc, value);
|
|
45
|
+
return needed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bool mik__kv_decode_i32(const uint8_t* in, size_t in_len, int32_t* out) {
|
|
49
|
+
if (!in || in_len == 0 || !out) return false;
|
|
50
|
+
nanocbor_value_t it;
|
|
51
|
+
nanocbor_decoder_init(&it, in, in_len);
|
|
52
|
+
return nanocbor_get_int32(&it, out) >= 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/* ── Device name pair ─────────────────────────────────────────────────────── */
|
|
57
|
+
|
|
58
|
+
namespace {
|
|
59
|
+
|
|
60
|
+
const char* skip_space(const char* p) {
|
|
61
|
+
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
|
62
|
+
return p;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
} // namespace
|
|
66
|
+
|
|
67
|
+
bool mik__device_name_parse(const char* json, int* out_rev, char* out_name, size_t name_len) {
|
|
68
|
+
if (!json || !out_rev || !out_name || name_len == 0) return false;
|
|
69
|
+
out_name[0] = '\0';
|
|
70
|
+
|
|
71
|
+
const char* p = skip_space(json);
|
|
72
|
+
if (*p != '[') return false;
|
|
73
|
+
p = skip_space(p + 1);
|
|
74
|
+
|
|
75
|
+
/* The revision. Negative and non-integer values are not pairs. */
|
|
76
|
+
if (*p < '0' || *p > '9') return false;
|
|
77
|
+
long rev = 0;
|
|
78
|
+
while (*p >= '0' && *p <= '9') {
|
|
79
|
+
rev = rev * 10 + (*p - '0');
|
|
80
|
+
if (rev > INT32_MAX) return false;
|
|
81
|
+
p++;
|
|
82
|
+
}
|
|
83
|
+
p = skip_space(p);
|
|
84
|
+
*out_rev = (int)rev;
|
|
85
|
+
|
|
86
|
+
if (*p == ']') return true; /* [rev] — cleared */
|
|
87
|
+
if (*p != ',') return false;
|
|
88
|
+
p = skip_space(p + 1);
|
|
89
|
+
|
|
90
|
+
/* A cleared name may also arrive as an explicit null. */
|
|
91
|
+
if (strncmp(p, "null", 4) == 0) {
|
|
92
|
+
p = skip_space(p + 4);
|
|
93
|
+
return *p == ']';
|
|
94
|
+
}
|
|
95
|
+
if (*p != '"') return false;
|
|
96
|
+
p++;
|
|
97
|
+
|
|
98
|
+
size_t written = 0;
|
|
99
|
+
while (*p && *p != '"') {
|
|
100
|
+
/* Escapes: the writer is JSON.stringify, so a name can carry them. Only
|
|
101
|
+
* the forms it emits are handled; anything else fails the parse rather
|
|
102
|
+
* than storing a half-decoded name. */
|
|
103
|
+
char c = *p;
|
|
104
|
+
if (c == '\\') {
|
|
105
|
+
p++;
|
|
106
|
+
switch (*p) {
|
|
107
|
+
case '"':
|
|
108
|
+
case '\\':
|
|
109
|
+
case '/':
|
|
110
|
+
c = *p;
|
|
111
|
+
break;
|
|
112
|
+
case 'n':
|
|
113
|
+
c = '\n';
|
|
114
|
+
break;
|
|
115
|
+
case 't':
|
|
116
|
+
c = '\t';
|
|
117
|
+
break;
|
|
118
|
+
case 'r':
|
|
119
|
+
c = '\r';
|
|
120
|
+
break;
|
|
121
|
+
case 'b':
|
|
122
|
+
c = '\b';
|
|
123
|
+
break;
|
|
124
|
+
case 'f':
|
|
125
|
+
c = '\f';
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (written + 1 >= name_len) return false;
|
|
132
|
+
out_name[written++] = c;
|
|
133
|
+
p++;
|
|
134
|
+
}
|
|
135
|
+
if (*p != '"') return false;
|
|
136
|
+
out_name[written] = '\0';
|
|
137
|
+
p = skip_space(p + 1);
|
|
138
|
+
return *p == ']';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
size_t mik__device_name_format(int rev, const char* name, char* out, size_t out_len) {
|
|
142
|
+
if (rev < 0) rev = 0;
|
|
143
|
+
if (!name || !name[0]) {
|
|
144
|
+
int needed = snprintf(nullptr, 0, "[%d]", rev);
|
|
145
|
+
if (out && out_len > (size_t)needed) snprintf(out, out_len, "[%d]", rev);
|
|
146
|
+
return (size_t)needed;
|
|
147
|
+
}
|
|
148
|
+
/* Escape what JSON requires. Control characters below 0x20 have no short
|
|
149
|
+
* form here, so they are dropped rather than emitted raw — a name is display
|
|
150
|
+
* text, and a device is not the place to grow a \u encoder. */
|
|
151
|
+
char escaped[128];
|
|
152
|
+
size_t w = 0;
|
|
153
|
+
for (const char* p = name; *p && w + 2 < sizeof(escaped); p++) {
|
|
154
|
+
unsigned char c = (unsigned char)*p;
|
|
155
|
+
if (c == '"' || c == '\\') {
|
|
156
|
+
escaped[w++] = '\\';
|
|
157
|
+
escaped[w++] = (char)c;
|
|
158
|
+
} else if (c >= 0x20) {
|
|
159
|
+
escaped[w++] = (char)c;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
escaped[w] = '\0';
|
|
163
|
+
|
|
164
|
+
int needed = snprintf(nullptr, 0, "[%d,\"%s\"]", rev, escaped);
|
|
165
|
+
if (out && out_len > (size_t)needed) snprintf(out, out_len, "[%d,\"%s\"]", rev, escaped);
|
|
166
|
+
return (size_t)needed;
|
|
167
|
+
}
|
package/src/mikrojs.cpp
CHANGED
|
@@ -16,7 +16,16 @@
|
|
|
16
16
|
#include "mikrojs/private.h"
|
|
17
17
|
#include "mikrojs/utils.h"
|
|
18
18
|
|
|
19
|
+
#ifndef __has_feature
|
|
20
|
+
#define __has_feature(x) 0
|
|
21
|
+
#endif
|
|
22
|
+
#if defined(__SANITIZE_ADDRESS__) || __has_feature(address_sanitizer)
|
|
23
|
+
/* ASan inflates native frames severalfold; the regular default overflows
|
|
24
|
+
* on shallow JS recursion in instrumented builds. Never active on device. */
|
|
25
|
+
#define MIK__DEFAULT_STACK_SIZE 8 * 1024 * 1024
|
|
26
|
+
#else
|
|
19
27
|
#define MIK__DEFAULT_STACK_SIZE 1024 * 1024 // 1 MB
|
|
28
|
+
#endif
|
|
20
29
|
|
|
21
30
|
/* JS malloc functions. Routed through the mik__js_* family so the QuickJS
|
|
22
31
|
* heap can be relocated to PSRAM on chips with both internal SRAM and PSRAM,
|
|
@@ -497,6 +506,12 @@ void MIK_RegisterNativeModuleInit(MIKRuntime* mik_rt, const char* name,
|
|
|
497
506
|
|
|
498
507
|
void MIK_RegisterLoopConsumer(MIKRuntime* mik_rt, MIKLoopConsumeFn consume_fn,
|
|
499
508
|
MIKLoopDestroyFn destroy_fn) {
|
|
509
|
+
/* Idempotent: a module can be brought up twice — once by native code that
|
|
510
|
+
* uses its machinery directly, once by the loader when JS imports it — and
|
|
511
|
+
* registering the pair twice would run its destroy twice at teardown. */
|
|
512
|
+
for (const auto& consumer : mik_rt->loop_consumers) {
|
|
513
|
+
if (consumer.consume_fn == consume_fn && consumer.destroy_fn == destroy_fn) return;
|
|
514
|
+
}
|
|
500
515
|
mik_rt->loop_consumers.push_back({consume_fn, destroy_fn});
|
|
501
516
|
}
|
|
502
517
|
|
package/src/modules.cpp
CHANGED
|
@@ -61,10 +61,10 @@ static JSModuleDef* mik__load_bjs_module(JSContext* ctx, const char* module_name
|
|
|
61
61
|
JS_FreeValue(ctx, func_val);
|
|
62
62
|
return NULL;
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
/* No JS_ResolveModule here: this runs inside the module loader, and its
|
|
65
|
+
* failure path frees every unresolved module in the context — including
|
|
66
|
+
* a partially-read parent an outer JS_ReadModule frame still holds. The
|
|
67
|
+
* root resolve covers loader-returned modules. */
|
|
68
68
|
js_module_set_import_meta(ctx, func_val, true, false);
|
|
69
69
|
JSModuleDef* m = static_cast<JSModuleDef*>(JS_VALUE_GET_PTR(func_val));
|
|
70
70
|
JS_FreeValue(ctx, func_val);
|
|
@@ -452,7 +452,19 @@ static const char* mik__resolve_export_condition(JSContext* ctx, JSValue exp) {
|
|
|
452
452
|
*
|
|
453
453
|
* TODO: check publishConfig.exports before exports
|
|
454
454
|
*/
|
|
455
|
-
|
|
455
|
+
/* Copy a JS_ToCString result into a js_malloc'd buffer, so every return
|
|
456
|
+
* from mik__resolve_pkg_entry uses one allocator — the "./index.js"
|
|
457
|
+
* fallback has no JSString to borrow, and mixing JS_FreeCString with
|
|
458
|
+
* js_strdup'd pointers walks back to a fake string header. */
|
|
459
|
+
static char* mik__dup_cstring(JSContext* ctx, const char* cstr) {
|
|
460
|
+
if (!cstr) return NULL;
|
|
461
|
+
char* copy = js_strdup(ctx, cstr);
|
|
462
|
+
JS_FreeCString(ctx, cstr);
|
|
463
|
+
return copy;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/* Returns a js_malloc'd entry path (caller frees with js_free), or NULL. */
|
|
467
|
+
static char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, const char* subpath) {
|
|
456
468
|
JSValue exports = JS_GetPropertyStr(ctx, pkg_json, "exports");
|
|
457
469
|
|
|
458
470
|
if (JS_IsUndefined(exports)) {
|
|
@@ -460,7 +472,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
|
|
|
460
472
|
/* No exports field — fall back to "main", then "./index.js" */
|
|
461
473
|
JSValue main_val = JS_GetPropertyStr(ctx, pkg_json, "main");
|
|
462
474
|
if (JS_IsString(main_val)) {
|
|
463
|
-
|
|
475
|
+
char* result = mik__dup_cstring(ctx, JS_ToCString(ctx, main_val));
|
|
464
476
|
JS_FreeValue(ctx, main_val);
|
|
465
477
|
return result;
|
|
466
478
|
}
|
|
@@ -470,7 +482,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
|
|
|
470
482
|
|
|
471
483
|
/* exports is a plain string — use it directly */
|
|
472
484
|
if (JS_IsString(exports)) {
|
|
473
|
-
|
|
485
|
+
char* result = mik__dup_cstring(ctx, JS_ToCString(ctx, exports));
|
|
474
486
|
JS_FreeValue(ctx, exports);
|
|
475
487
|
return result;
|
|
476
488
|
}
|
|
@@ -479,7 +491,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
|
|
|
479
491
|
if (JS_IsObject(exports)) {
|
|
480
492
|
JSValue subpath_val = JS_GetPropertyStr(ctx, exports, subpath);
|
|
481
493
|
if (!JS_IsUndefined(subpath_val) && !JS_IsException(subpath_val)) {
|
|
482
|
-
|
|
494
|
+
char* result = mik__dup_cstring(ctx, mik__resolve_export_condition(ctx, subpath_val));
|
|
483
495
|
JS_FreeValue(ctx, subpath_val);
|
|
484
496
|
JS_FreeValue(ctx, exports);
|
|
485
497
|
return result;
|
|
@@ -490,7 +502,7 @@ static const char* mik__resolve_pkg_entry(JSContext* ctx, JSValue pkg_json, cons
|
|
|
490
502
|
* itself as a conditions object. This handles the common shorthand where
|
|
491
503
|
* exports is {"default": "./index.js"} instead of {".": {"default": ...}} */
|
|
492
504
|
if (strcmp(subpath, ".") == 0) {
|
|
493
|
-
|
|
505
|
+
char* result = mik__dup_cstring(ctx, mik__resolve_export_condition(ctx, exports));
|
|
494
506
|
JS_FreeValue(ctx, exports);
|
|
495
507
|
return result;
|
|
496
508
|
}
|
|
@@ -593,7 +605,7 @@ static char* mik__try_resolve_pkg(JSContext* ctx, const char* dir, const MIKPkgS
|
|
|
593
605
|
return NULL;
|
|
594
606
|
}
|
|
595
607
|
|
|
596
|
-
|
|
608
|
+
char* entry = mik__resolve_pkg_entry(ctx, pkg_json, spec->subpath);
|
|
597
609
|
JS_FreeValue(ctx, pkg_json);
|
|
598
610
|
|
|
599
611
|
if (!entry) {
|
|
@@ -608,7 +620,7 @@ static char* mik__try_resolve_pkg(JSContext* ctx, const char* dir, const MIKPkgS
|
|
|
608
620
|
char result[PATH_MAX];
|
|
609
621
|
snprintf(result, sizeof(result), "%s/node_modules/%.*s/%s", dir, spec->pkg_name_len,
|
|
610
622
|
spec->pkg_name, entry_path);
|
|
611
|
-
|
|
623
|
+
js_free(ctx, entry);
|
|
612
624
|
return js_strdup(ctx, result);
|
|
613
625
|
}
|
|
614
626
|
|
|
@@ -754,6 +766,8 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
|
|
|
754
766
|
} else {
|
|
755
767
|
len = 0;
|
|
756
768
|
}
|
|
769
|
+
/* A rooted base ('/main.js') has dirname '/', not "" */
|
|
770
|
+
bool rooted = base_name[0] == MIK__PATHSEP_POSIX;
|
|
757
771
|
|
|
758
772
|
filename = static_cast<char*>(js_malloc(ctx, len + strlen(name) + 1 + 1));
|
|
759
773
|
if (!filename) {
|
|
@@ -771,7 +785,12 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
|
|
|
771
785
|
/* remove the last path element of filename, except if "."
|
|
772
786
|
or ".." */
|
|
773
787
|
if (filename[0] == '\0') {
|
|
774
|
-
|
|
788
|
+
if (!rooted) {
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
/* the parent of the root is the root */
|
|
792
|
+
r += 3;
|
|
793
|
+
continue;
|
|
775
794
|
}
|
|
776
795
|
p = strrchr(filename, MIK__PATHSEP);
|
|
777
796
|
if (!p) {
|
|
@@ -791,7 +810,7 @@ static char* mik__module_normalizer_impl(JSContext* ctx, const char* base_name,
|
|
|
791
810
|
break;
|
|
792
811
|
}
|
|
793
812
|
}
|
|
794
|
-
if (filename[0] != '\0') {
|
|
813
|
+
if (filename[0] != '\0' || rooted) {
|
|
795
814
|
strcat(filename, MIK__PATHSEP_STR);
|
|
796
815
|
}
|
|
797
816
|
strcat(filename, r);
|