@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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#include "mikrojs/ota_policy.h"
|
|
2
|
+
|
|
3
|
+
#include <cstdio>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
#include <memory>
|
|
6
|
+
#include <string>
|
|
7
|
+
|
|
8
|
+
#include "mikrojs/platform.h"
|
|
9
|
+
|
|
10
|
+
namespace mikrojs {
|
|
11
|
+
|
|
12
|
+
/* Staging attempts for one url before a checksum is abandoned. */
|
|
13
|
+
constexpr int32_t MIK__OTA_MAX_TRIES = 3;
|
|
14
|
+
|
|
15
|
+
const char* mik__ota_outcome_to_str(MIKOtaApplyOutcome outcome) {
|
|
16
|
+
switch (outcome) {
|
|
17
|
+
case MIKOtaApplyOutcome::kStaged:
|
|
18
|
+
return "staged";
|
|
19
|
+
case MIKOtaApplyOutcome::kTrialPending:
|
|
20
|
+
return "trial-pending";
|
|
21
|
+
case MIKOtaApplyOutcome::kCurrent:
|
|
22
|
+
return "current";
|
|
23
|
+
case MIKOtaApplyOutcome::kAbandoned:
|
|
24
|
+
return "abandoned";
|
|
25
|
+
case MIKOtaApplyOutcome::kExhausted:
|
|
26
|
+
return "exhausted";
|
|
27
|
+
}
|
|
28
|
+
return "unknown";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── MIKOtaStore ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
/* FNV-1a, rendered as hex. Identity for a cache key, not a security property:
|
|
36
|
+
* the worst a collision does is deny one build a fresh retry budget. */
|
|
37
|
+
std::string url_digest(const std::string& url) {
|
|
38
|
+
uint64_t hash = 1469598103934665603ull;
|
|
39
|
+
for (unsigned char c : url) {
|
|
40
|
+
hash ^= c;
|
|
41
|
+
hash *= 1099511628211ull;
|
|
42
|
+
}
|
|
43
|
+
char out[17];
|
|
44
|
+
snprintf(out, sizeof(out), "%016llx", static_cast<unsigned long long>(hash));
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
} // namespace
|
|
49
|
+
|
|
50
|
+
bool MIKOtaStore::UrlMatches(const std::string& url) const {
|
|
51
|
+
if (!env_ || !env_->kv_get_str) return false;
|
|
52
|
+
char buf[17] = {};
|
|
53
|
+
if (!env_->kv_get_str(env_->opaque, "ota.url", buf, sizeof(buf))) return false;
|
|
54
|
+
return url_digest(url) == buf;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
void MIKOtaStore::SetUrl(const std::string& url) {
|
|
58
|
+
if (!env_ || !env_->kv_set_str) return;
|
|
59
|
+
env_->kv_set_str(env_->opaque, "ota.url", url_digest(url).c_str());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
std::string MIKOtaStore::GetAttempt() const {
|
|
63
|
+
if (!env_ || !env_->kv_get_str) return "";
|
|
64
|
+
char buf[128] = {};
|
|
65
|
+
if (env_->kv_get_str(env_->opaque, "ota.att", buf, sizeof(buf))) {
|
|
66
|
+
return buf;
|
|
67
|
+
}
|
|
68
|
+
return "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void MIKOtaStore::SetAttempt(const std::string& checksum) {
|
|
72
|
+
if (!env_ || !env_->kv_set_str) return;
|
|
73
|
+
env_->kv_set_str(env_->opaque, "ota.att", checksum.c_str());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
int32_t MIKOtaStore::GetTries() const {
|
|
77
|
+
if (!env_ || !env_->kv_get_i32) return 0;
|
|
78
|
+
int32_t val = 0;
|
|
79
|
+
if (env_->kv_get_i32(env_->opaque, "ota.tries", &val)) {
|
|
80
|
+
return val;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void MIKOtaStore::SetTries(int32_t n) {
|
|
86
|
+
if (!env_ || !env_->kv_set_i32) return;
|
|
87
|
+
env_->kv_set_i32(env_->opaque, "ota.tries", n);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
std::string MIKOtaStore::GetBad() const {
|
|
91
|
+
if (!env_ || !env_->kv_get_str) return "";
|
|
92
|
+
char buf[128] = {};
|
|
93
|
+
if (env_->kv_get_str(env_->opaque, "ota.bad", buf, sizeof(buf))) {
|
|
94
|
+
return buf;
|
|
95
|
+
}
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
void MIKOtaStore::SetBad(const std::string& checksum) {
|
|
100
|
+
if (!env_ || !env_->kv_set_str) return;
|
|
101
|
+
env_->kv_set_str(env_->opaque, "ota.bad", checksum.c_str());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
bool MIKOtaStore::GetInFlight() const {
|
|
105
|
+
if (!env_ || !env_->kv_get_i32) return false;
|
|
106
|
+
int32_t val = 0;
|
|
107
|
+
if (env_->kv_get_i32(env_->opaque, "ota.inflight", &val)) {
|
|
108
|
+
return val == 1;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void MIKOtaStore::SetInFlight(bool in_flight) {
|
|
114
|
+
if (!env_ || !env_->kv_set_i32) return;
|
|
115
|
+
env_->kv_set_i32(env_->opaque, "ota.inflight", in_flight ? 1 : 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Offer parsing ────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
bool mik__ota_parse_offer(const char* url, const char* checksum, int64_t size, bool allow_insecure,
|
|
121
|
+
MIKOtaOffer* out_offer, std::string* out_warn_reason) {
|
|
122
|
+
if (!url) return false;
|
|
123
|
+
|
|
124
|
+
std::string s_url = url;
|
|
125
|
+
bool is_https = s_url.rfind("https://", 0) == 0;
|
|
126
|
+
bool is_http = s_url.rfind("http://", 0) == 0;
|
|
127
|
+
bool scheme_ok = is_https || (allow_insecure && is_http);
|
|
128
|
+
|
|
129
|
+
size_t query_pos = s_url.find_first_of("?#");
|
|
130
|
+
std::string path = (query_pos == std::string::npos) ? s_url : s_url.substr(0, query_pos);
|
|
131
|
+
|
|
132
|
+
bool ends_with_tgz = path.size() >= 4 && path.compare(path.size() - 4, 4, ".tgz") == 0;
|
|
133
|
+
|
|
134
|
+
if (!scheme_ok || !ends_with_tgz) {
|
|
135
|
+
if (out_warn_reason) {
|
|
136
|
+
*out_warn_reason =
|
|
137
|
+
allow_insecure ? "url must be an http(s) .tgz" : "url must be an https .tgz";
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!checksum || strlen(checksum) == 0) {
|
|
143
|
+
if (out_warn_reason) {
|
|
144
|
+
*out_warn_reason = "missing checksum";
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (size <= 0) {
|
|
150
|
+
if (out_warn_reason) {
|
|
151
|
+
*out_warn_reason = "invalid size";
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (out_offer) {
|
|
157
|
+
out_offer->url = s_url;
|
|
158
|
+
out_offer->checksum = checksum;
|
|
159
|
+
out_offer->size = static_cast<size_t>(size);
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Policy Operations ────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
MIKOtaReconcileOutcome mik__ota_policy_reconcile(const MIKOtaEnv* env) {
|
|
167
|
+
MIKOtaStore store(env);
|
|
168
|
+
if (store.GetInFlight()) {
|
|
169
|
+
store.SetInFlight(false);
|
|
170
|
+
} else {
|
|
171
|
+
store.SetTries(0);
|
|
172
|
+
}
|
|
173
|
+
MIKOtaReconcileOutcome outcome = {};
|
|
174
|
+
if (env && env->reconcile) {
|
|
175
|
+
env->reconcile(env->opaque, &outcome);
|
|
176
|
+
}
|
|
177
|
+
return outcome;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
MIKOtaRunningBuild mik__ota_policy_running(const MIKOtaEnv* env) {
|
|
181
|
+
MIKOtaRunningBuild run = {};
|
|
182
|
+
if (env && env->running) {
|
|
183
|
+
env->running(env->opaque, &run);
|
|
184
|
+
}
|
|
185
|
+
if (strlen(run.version) == 0 && env && env->read_app_version) {
|
|
186
|
+
env->read_app_version(env->opaque, run.version, sizeof(run.version));
|
|
187
|
+
}
|
|
188
|
+
return run;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
bool mik__ota_policy_revert(const MIKOtaEnv* env, MIKOtaError* out_err) {
|
|
192
|
+
char err_buf[256] = {};
|
|
193
|
+
if (env && env->revert && env->revert(env->opaque, err_buf, sizeof(err_buf))) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (out_err) {
|
|
197
|
+
out_err->name = "InstallFailed";
|
|
198
|
+
out_err->kind = "transient";
|
|
199
|
+
out_err->message = err_buf[0] ? err_buf : "revert failed";
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
void mik__ota_policy_confirm(const MIKOtaEnv* env) {
|
|
205
|
+
if (env && env->mark_valid) {
|
|
206
|
+
env->mark_valid(env->opaque);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
namespace {
|
|
211
|
+
|
|
212
|
+
/* The staging session handed to the download pump. Bounds every write against
|
|
213
|
+
* the offered size, counting bytes already on flash from a resumed transfer. */
|
|
214
|
+
class UpdateImpl : public MIKOtaUpdate {
|
|
215
|
+
public:
|
|
216
|
+
UpdateImpl(const MIKOtaEnv* env, size_t size, size_t resume_offset)
|
|
217
|
+
: env_(env), size_(size), resume_offset_(resume_offset), written_(resume_offset) {}
|
|
218
|
+
|
|
219
|
+
size_t resume_offset() const override { return resume_offset_; }
|
|
220
|
+
|
|
221
|
+
bool write(const uint8_t* bytes, size_t len, MIKOtaError* out_err) override {
|
|
222
|
+
if (written_ + len > size_) {
|
|
223
|
+
if (out_err) {
|
|
224
|
+
out_err->name = "TooLarge";
|
|
225
|
+
char buf[128];
|
|
226
|
+
snprintf(buf, sizeof(buf), "build exceeds offered size %zu", size_);
|
|
227
|
+
out_err->message = buf;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
char eb[256] = {};
|
|
232
|
+
if (!env_ || !env_->stage_write ||
|
|
233
|
+
!env_->stage_write(env_->opaque, bytes, len, eb, sizeof(eb))) {
|
|
234
|
+
if (out_err) {
|
|
235
|
+
out_err->name = "StagingFull";
|
|
236
|
+
out_err->message = eb[0] ? eb : "staging full";
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
written_ += len;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
bool finish(const MIKOtaInstallOptions& opts, MIKOtaError* out_err) override {
|
|
245
|
+
char eb[256] = {};
|
|
246
|
+
int err_kind = 0;
|
|
247
|
+
if (!env_ || !env_->stage_finish ||
|
|
248
|
+
!env_->stage_finish(env_->opaque, opts.trial_boots, opts.require_confirm,
|
|
249
|
+
opts.install_now, eb, sizeof(eb), &err_kind)) {
|
|
250
|
+
if (out_err) {
|
|
251
|
+
out_err->name = "InstallFailed";
|
|
252
|
+
out_err->kind = (err_kind == MIK_OTA_ERR_CORRUPT) ? "corrupt"
|
|
253
|
+
: (err_kind == MIK_OTA_ERR_OOM) ? "oom"
|
|
254
|
+
: "transient";
|
|
255
|
+
out_err->message = eb[0] ? eb : "install failed";
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
void abort() override {
|
|
263
|
+
if (env_ && env_->stage_abort) {
|
|
264
|
+
env_->stage_abort(env_->opaque);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private:
|
|
269
|
+
const MIKOtaEnv* env_;
|
|
270
|
+
size_t size_;
|
|
271
|
+
size_t resume_offset_;
|
|
272
|
+
size_t written_;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
} // namespace
|
|
276
|
+
|
|
277
|
+
bool mik__ota_policy_apply_begin(const MIKOtaEnv* env, const MIKOtaOffer& offer,
|
|
278
|
+
MIKOtaApplySession* session, MIKOtaApplyOutcome* out_outcome,
|
|
279
|
+
MIKOtaError* out_err) {
|
|
280
|
+
session->env = env;
|
|
281
|
+
session->checksum = offer.checksum;
|
|
282
|
+
session->update.reset();
|
|
283
|
+
|
|
284
|
+
MIKOtaRunningBuild run = {};
|
|
285
|
+
if (env && env->running) {
|
|
286
|
+
env->running(env->opaque, &run);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// (a) a trial is unresolved. Reported distinctly because the caller's
|
|
290
|
+
// response differs from every other skip: the build on trial still needs
|
|
291
|
+
// confirming, and treating this like "nothing to do" lets the trial lapse
|
|
292
|
+
// and roll back a healthy build just because a newer one was published.
|
|
293
|
+
if (run.trial) {
|
|
294
|
+
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kTrialPending;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// (b) already running this build
|
|
299
|
+
if (!offer.checksum.empty() && offer.checksum == run.checksum) {
|
|
300
|
+
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kCurrent;
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// (d) device has given up on this build
|
|
305
|
+
MIKOtaStore store(env);
|
|
306
|
+
if (!offer.checksum.empty() && offer.checksum == store.GetBad()) {
|
|
307
|
+
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kAbandoned;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// (e) retry budget, keyed on url *and* checksum. Exhausting it only stops
|
|
312
|
+
// attempts against this exact build at this exact url; the checksum is not
|
|
313
|
+
// abandoned, because everything counted here is transient by construction
|
|
314
|
+
// (a corrupt build is abandoned in apply_end instead).
|
|
315
|
+
if (!store.UrlMatches(offer.url) || offer.checksum != store.GetAttempt()) {
|
|
316
|
+
store.SetUrl(offer.url);
|
|
317
|
+
store.SetAttempt(offer.checksum);
|
|
318
|
+
store.SetTries(0);
|
|
319
|
+
}
|
|
320
|
+
int32_t tries = store.GetTries();
|
|
321
|
+
if (tries >= MIK__OTA_MAX_TRIES) {
|
|
322
|
+
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kExhausted;
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// (f) bump before the attempt, so an attempt that crashes the device still
|
|
327
|
+
// counts — the in-flight flag is what carries that across the reboot (see
|
|
328
|
+
// mik__ota_policy_reconcile). apply_end clears it on every ordinary exit,
|
|
329
|
+
// and deliberately never runs when the attempt takes the device down.
|
|
330
|
+
store.SetTries(tries + 1);
|
|
331
|
+
store.SetInFlight(true);
|
|
332
|
+
|
|
333
|
+
// (g) open staging
|
|
334
|
+
size_t resume_offset = 0;
|
|
335
|
+
char err_buf[256] = {};
|
|
336
|
+
if (!env || !env->stage_begin ||
|
|
337
|
+
!env->stage_begin(env->opaque, offer.checksum.c_str(), offer.size, &resume_offset, err_buf,
|
|
338
|
+
sizeof(err_buf))) {
|
|
339
|
+
store.SetInFlight(false);
|
|
340
|
+
if (out_err) {
|
|
341
|
+
out_err->name = "StagingFailed";
|
|
342
|
+
out_err->message = err_buf[0] ? err_buf : "staging failed";
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
session->update = std::make_unique<UpdateImpl>(env, offer.size, resume_offset);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
bool mik__ota_policy_apply_end(MIKOtaApplySession* session, bool download_ok,
|
|
352
|
+
const std::string& download_message,
|
|
353
|
+
const MIKOtaInstallOptions& options,
|
|
354
|
+
MIKOtaApplyOutcome* out_outcome, MIKOtaError* out_err) {
|
|
355
|
+
MIKOtaStore store(session->env);
|
|
356
|
+
MIKOtaUpdate* update = session->update.get();
|
|
357
|
+
struct Closer {
|
|
358
|
+
MIKOtaApplySession* s;
|
|
359
|
+
MIKOtaStore& store;
|
|
360
|
+
~Closer() {
|
|
361
|
+
s->update.reset();
|
|
362
|
+
store.SetInFlight(false);
|
|
363
|
+
}
|
|
364
|
+
} closer{session, store};
|
|
365
|
+
|
|
366
|
+
if (!update) {
|
|
367
|
+
if (out_err) {
|
|
368
|
+
out_err->name = "StagingFailed";
|
|
369
|
+
out_err->message = "no staging session";
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// A failure in the download is transient whatever tripped it: keep the
|
|
375
|
+
// bumped tries so it retries next time, and do NOT abandon the checksum
|
|
376
|
+
// (that is only for a corrupt build). The pump's own error name — a
|
|
377
|
+
// TooLarge or StagingFull from a write — is folded into the message, so
|
|
378
|
+
// the caller can key on DownloadFailed alone.
|
|
379
|
+
if (!download_ok) {
|
|
380
|
+
update->abort();
|
|
381
|
+
if (out_err) {
|
|
382
|
+
out_err->name = "DownloadFailed";
|
|
383
|
+
out_err->kind.clear();
|
|
384
|
+
out_err->message = download_message;
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// A corrupt build is the one thing worth abandoning: the same bytes will
|
|
390
|
+
// fail identically forever. Abort first, or the verified-bad staging file
|
|
391
|
+
// (up to the whole build) sits on the app partition until some later offer
|
|
392
|
+
// happens to reclaim it.
|
|
393
|
+
if (!update->finish(options, out_err)) {
|
|
394
|
+
if (out_err && out_err->name == "InstallFailed" && out_err->kind == "corrupt") {
|
|
395
|
+
update->abort();
|
|
396
|
+
store.SetBad(session->checksum);
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
store.SetTries(0);
|
|
402
|
+
if (out_outcome) *out_outcome = MIKOtaApplyOutcome::kStaged;
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
bool mik__ota_policy_apply_offer(const MIKOtaEnv* env, const MIKOtaOffer& offer,
|
|
407
|
+
MIKOtaDownloadFn download, const MIKOtaInstallOptions& options,
|
|
408
|
+
MIKOtaApplyOutcome* out_outcome, MIKOtaError* out_err) {
|
|
409
|
+
MIKOtaApplySession session;
|
|
410
|
+
if (!mik__ota_policy_apply_begin(env, offer, &session, out_outcome, out_err)) return false;
|
|
411
|
+
if (!session.update) return true; // declined before staging
|
|
412
|
+
|
|
413
|
+
MIKOtaError dl_err;
|
|
414
|
+
bool downloaded = download(*session.update, &dl_err);
|
|
415
|
+
std::string message = dl_err.message.empty() ? "download failed" : dl_err.message;
|
|
416
|
+
return mik__ota_policy_apply_end(&session, downloaded, message, options, out_outcome, out_err);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
} // namespace mikrojs
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#include "mikrojs/ota_slots.h"
|
|
2
|
+
|
|
3
|
+
#include <nanocbor/nanocbor.h>
|
|
4
|
+
|
|
5
|
+
#include <cstring>
|
|
6
|
+
#include <string>
|
|
7
|
+
|
|
8
|
+
#include "mikrojs/cbor_helpers.h"
|
|
9
|
+
|
|
10
|
+
namespace mikrojs {
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
const char* slot_key(MIKOtaConfigSlot slot) {
|
|
15
|
+
switch (slot) {
|
|
16
|
+
case MIK_OTA_CFG_CURRENT:
|
|
17
|
+
return "ota.cfg";
|
|
18
|
+
case MIK_OTA_CFG_NEXT:
|
|
19
|
+
return "ota.cfgNext";
|
|
20
|
+
case MIK_OTA_CFG_PREV:
|
|
21
|
+
return "ota.cfgPrev";
|
|
22
|
+
}
|
|
23
|
+
return "ota.cfg";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const char* kTrialKey = "ota.cfgTrial";
|
|
27
|
+
const char* kErrorKey = "ota.cfgErr";
|
|
28
|
+
|
|
29
|
+
/* Read a whole blob, distinguishing absent from failed. */
|
|
30
|
+
MIKOtaKvStatus kv_read(const MIKOtaEnv* env, const char* key, std::vector<uint8_t>* out) {
|
|
31
|
+
if (!env || !env->kv_get_blob) return MIK_OTA_KV_ABSENT;
|
|
32
|
+
size_t len = 0;
|
|
33
|
+
MIKOtaKvStatus status = env->kv_get_blob(env->opaque, key, nullptr, &len);
|
|
34
|
+
if (status != MIK_OTA_KV_OK) return status;
|
|
35
|
+
if (len == 0) return MIK_OTA_KV_ABSENT;
|
|
36
|
+
out->resize(len);
|
|
37
|
+
status = env->kv_get_blob(env->opaque, key, out->data(), &len);
|
|
38
|
+
if (status != MIK_OTA_KV_OK) return status;
|
|
39
|
+
out->resize(len);
|
|
40
|
+
return MIK_OTA_KV_OK;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bool take_str(nanocbor_value_t* it, char* out, size_t out_len) {
|
|
44
|
+
const uint8_t* ptr = nullptr;
|
|
45
|
+
size_t len = 0;
|
|
46
|
+
if (nanocbor_get_tstr(it, &ptr, &len) < 0) return false;
|
|
47
|
+
snprintf(out, out_len, "%.*s", static_cast<int>(len), reinterpret_cast<const char*>(ptr));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bool take_key(nanocbor_value_t* map, std::string* out) {
|
|
52
|
+
const uint8_t* ptr = nullptr;
|
|
53
|
+
size_t len = 0;
|
|
54
|
+
if (nanocbor_get_tstr(map, &ptr, &len) < 0) return false;
|
|
55
|
+
out->assign(reinterpret_cast<const char*>(ptr), len);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
template <typename Fn>
|
|
60
|
+
std::vector<uint8_t> encode_to_vector(Fn&& encode) {
|
|
61
|
+
/* The measuring pass needs a non-null base: zero-length appends (an empty
|
|
62
|
+
* string, an empty document) still reach memcpy. */
|
|
63
|
+
static uint8_t measure_base;
|
|
64
|
+
nanocbor_encoder_t enc;
|
|
65
|
+
nanocbor_encoder_init(&enc, &measure_base, 0);
|
|
66
|
+
encode(&enc);
|
|
67
|
+
std::vector<uint8_t> out(nanocbor_encoded_len(&enc));
|
|
68
|
+
nanocbor_encoder_init(&enc, out.data(), out.size());
|
|
69
|
+
encode(&enc);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Append pre-encoded CBOR verbatim. A stored document keeps the exact bytes the
|
|
74
|
+
* registry sent, so the JS reader decodes the same value the app would have
|
|
75
|
+
* seen; re-encoding would mean carrying a value model for no reason. */
|
|
76
|
+
void put_raw(nanocbor_encoder_t* enc, const uint8_t* data, size_t len) {
|
|
77
|
+
enc->len += len;
|
|
78
|
+
if (enc->fits(enc, enc->context, len)) {
|
|
79
|
+
enc->append(enc, enc->context, data, len);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
} // namespace
|
|
84
|
+
|
|
85
|
+
/* The slot holds the CBOR map the registry path wrote; anything else reads as
|
|
86
|
+
* absent. */
|
|
87
|
+
MIKOtaLoadedConfig mik__ota_load_slot(const MIKOtaEnv* env, MIKOtaConfigSlot slot) {
|
|
88
|
+
MIKOtaLoadedConfig out;
|
|
89
|
+
MIKOtaKvStatus status = kv_read(env, slot_key(slot), &out.bytes);
|
|
90
|
+
if (status == MIK_OTA_KV_ERROR) {
|
|
91
|
+
out.failed = true;
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
if (status != MIK_OTA_KV_OK) return out;
|
|
95
|
+
|
|
96
|
+
nanocbor_value_t val;
|
|
97
|
+
nanocbor_decoder_init(&val, out.bytes.data(), out.bytes.size());
|
|
98
|
+
nanocbor_value_t map;
|
|
99
|
+
if (nanocbor_get_type(&val) != NANOCBOR_TYPE_MAP || nanocbor_enter_map(&val, &map) < 0) {
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
while (!nanocbor_at_end(&map)) {
|
|
104
|
+
std::string key;
|
|
105
|
+
if (!take_key(&map, &key)) break;
|
|
106
|
+
if (key == "version") {
|
|
107
|
+
if (!take_str(&map, out.cfg.version, sizeof(out.cfg.version))) {
|
|
108
|
+
mik__cbor_skip_value(&map);
|
|
109
|
+
}
|
|
110
|
+
} else if (key == "rev") {
|
|
111
|
+
if (!take_str(&map, out.cfg.rev, sizeof(out.cfg.rev))) mik__cbor_skip_value(&map);
|
|
112
|
+
} else if (key == "doc") {
|
|
113
|
+
const uint8_t* start = map.cur;
|
|
114
|
+
if (nanocbor_get_null(&map) >= 0) continue; // null normalises to absent
|
|
115
|
+
if (mik__cbor_skip_value(&map) < 0) break;
|
|
116
|
+
out.cfg.doc_cbor = const_cast<uint8_t*>(start);
|
|
117
|
+
out.cfg.doc_cbor_len = static_cast<size_t>(map.cur - start);
|
|
118
|
+
} else {
|
|
119
|
+
mik__cbor_skip_value(&map);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
out.present = out.cfg.version[0] != '\0';
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
void mik__ota_store_slot(const MIKOtaEnv* env, MIKOtaConfigSlot slot,
|
|
127
|
+
const MIKOtaStoredConfig& cfg) {
|
|
128
|
+
if (!env || !env->kv_set_blob) return;
|
|
129
|
+
size_t map_size = 1; // version
|
|
130
|
+
if (cfg.rev[0]) map_size++;
|
|
131
|
+
if (cfg.doc_cbor && cfg.doc_cbor_len > 0) map_size++;
|
|
132
|
+
|
|
133
|
+
std::vector<uint8_t> buf = encode_to_vector([&](nanocbor_encoder_t* enc) {
|
|
134
|
+
nanocbor_fmt_map(enc, map_size);
|
|
135
|
+
nanocbor_put_tstr(enc, "version");
|
|
136
|
+
nanocbor_put_tstr(enc, cfg.version);
|
|
137
|
+
if (cfg.rev[0]) {
|
|
138
|
+
nanocbor_put_tstr(enc, "rev");
|
|
139
|
+
nanocbor_put_tstr(enc, cfg.rev);
|
|
140
|
+
}
|
|
141
|
+
if (cfg.doc_cbor && cfg.doc_cbor_len > 0) {
|
|
142
|
+
nanocbor_put_tstr(enc, "doc");
|
|
143
|
+
put_raw(enc, cfg.doc_cbor, cfg.doc_cbor_len);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
env->kv_set_blob(env->opaque, slot_key(slot), buf.data(), buf.size());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
void mik__ota_clear_slot(const MIKOtaEnv* env, MIKOtaConfigSlot slot) {
|
|
150
|
+
if (env && env->kv_remove) env->kv_remove(env->opaque, slot_key(slot));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
MIKOtaKvStatus mik__ota_load_trial(const MIKOtaEnv* env, MIKOtaConfigTrial* out) {
|
|
154
|
+
std::vector<uint8_t> bytes;
|
|
155
|
+
MIKOtaKvStatus status = kv_read(env, kTrialKey, &bytes);
|
|
156
|
+
if (status != MIK_OTA_KV_OK) return status;
|
|
157
|
+
nanocbor_value_t val;
|
|
158
|
+
nanocbor_decoder_init(&val, bytes.data(), bytes.size());
|
|
159
|
+
nanocbor_value_t map;
|
|
160
|
+
if (nanocbor_get_type(&val) != NANOCBOR_TYPE_MAP || nanocbor_enter_map(&val, &map) < 0) {
|
|
161
|
+
return MIK_OTA_KV_ABSENT;
|
|
162
|
+
}
|
|
163
|
+
bool has_left = false;
|
|
164
|
+
bool has_read = false;
|
|
165
|
+
while (!nanocbor_at_end(&map)) {
|
|
166
|
+
std::string key;
|
|
167
|
+
if (!take_key(&map, &key)) break;
|
|
168
|
+
if (key == "left") {
|
|
169
|
+
int32_t left = 0;
|
|
170
|
+
if (nanocbor_get_int32(&map, &left) >= 0) {
|
|
171
|
+
out->left = left;
|
|
172
|
+
has_left = true;
|
|
173
|
+
} else {
|
|
174
|
+
mik__cbor_skip_value(&map);
|
|
175
|
+
}
|
|
176
|
+
} else if (key == "read") {
|
|
177
|
+
bool read = false;
|
|
178
|
+
if (nanocbor_get_bool(&map, &read) >= 0) {
|
|
179
|
+
out->read = read;
|
|
180
|
+
has_read = true;
|
|
181
|
+
} else {
|
|
182
|
+
mik__cbor_skip_value(&map);
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
mik__cbor_skip_value(&map);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return (has_left && has_read) ? MIK_OTA_KV_OK : MIK_OTA_KV_ABSENT;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
void mik__ota_store_trial(const MIKOtaEnv* env, const MIKOtaConfigTrial& trial) {
|
|
192
|
+
if (!env || !env->kv_set_blob) return;
|
|
193
|
+
std::vector<uint8_t> buf = encode_to_vector([&](nanocbor_encoder_t* enc) {
|
|
194
|
+
nanocbor_fmt_map(enc, 2);
|
|
195
|
+
nanocbor_put_tstr(enc, "left");
|
|
196
|
+
nanocbor_fmt_int(enc, trial.left);
|
|
197
|
+
nanocbor_put_tstr(enc, "read");
|
|
198
|
+
nanocbor_fmt_bool(enc, trial.read);
|
|
199
|
+
});
|
|
200
|
+
env->kv_set_blob(env->opaque, kTrialKey, buf.data(), buf.size());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
void mik__ota_clear_trial(const MIKOtaEnv* env) {
|
|
204
|
+
if (env && env->kv_remove) env->kv_remove(env->opaque, kTrialKey);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
bool mik__ota_load_config_error(const MIKOtaEnv* env, MIKOtaConfigErrorReport* out) {
|
|
208
|
+
std::vector<uint8_t> bytes;
|
|
209
|
+
if (kv_read(env, kErrorKey, &bytes) != MIK_OTA_KV_OK) return false;
|
|
210
|
+
nanocbor_value_t val;
|
|
211
|
+
nanocbor_decoder_init(&val, bytes.data(), bytes.size());
|
|
212
|
+
nanocbor_value_t map;
|
|
213
|
+
if (nanocbor_get_type(&val) != NANOCBOR_TYPE_MAP || nanocbor_enter_map(&val, &map) < 0) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
memset(out, 0, sizeof(*out));
|
|
217
|
+
bool has_message = false;
|
|
218
|
+
while (!nanocbor_at_end(&map)) {
|
|
219
|
+
std::string key;
|
|
220
|
+
if (!take_key(&map, &key)) break;
|
|
221
|
+
if (key == "rev") {
|
|
222
|
+
if (!take_str(&map, out->rev, sizeof(out->rev))) mik__cbor_skip_value(&map);
|
|
223
|
+
} else if (key == "message") {
|
|
224
|
+
has_message = take_str(&map, out->message, sizeof(out->message));
|
|
225
|
+
if (!has_message) mik__cbor_skip_value(&map);
|
|
226
|
+
} else {
|
|
227
|
+
mik__cbor_skip_value(&map);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return out->rev[0] != '\0' && has_message;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
void mik__ota_store_config_error(const MIKOtaEnv* env, const MIKOtaConfigErrorReport& report) {
|
|
234
|
+
if (!env || !env->kv_set_blob) return;
|
|
235
|
+
std::vector<uint8_t> buf = encode_to_vector([&](nanocbor_encoder_t* enc) {
|
|
236
|
+
nanocbor_fmt_map(enc, 2);
|
|
237
|
+
nanocbor_put_tstr(enc, "rev");
|
|
238
|
+
nanocbor_put_tstr(enc, report.rev);
|
|
239
|
+
nanocbor_put_tstr(enc, "message");
|
|
240
|
+
nanocbor_put_tstr(enc, report.message);
|
|
241
|
+
});
|
|
242
|
+
env->kv_set_blob(env->opaque, kErrorKey, buf.data(), buf.size());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
void mik__ota_clear_config_error(const MIKOtaEnv* env) {
|
|
246
|
+
if (env && env->kv_remove) env->kv_remove(env->opaque, kErrorKey);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
} // namespace mikrojs
|
package/src/mik_repl.cpp
CHANGED
|
@@ -852,9 +852,12 @@ static std::vector<uint8_t> proto_complete(JSContext* ctx, const char* partial,
|
|
|
852
852
|
const char* argv[1] = {partial};
|
|
853
853
|
completion_callback(cb, 1, argv);
|
|
854
854
|
|
|
855
|
-
/* Two-pass CBOR encode: {"prefix": tstr, "items": [tstr, ...]}
|
|
855
|
+
/* Two-pass CBOR encode: {"prefix": tstr, "items": [tstr, ...]}. The
|
|
856
|
+
* measuring pass needs a non-null base: zero-length appends still hit
|
|
857
|
+
* memcpy, and memcpy(NULL, ..., 0) is undefined behavior. */
|
|
858
|
+
static uint8_t measure_base;
|
|
856
859
|
nanocbor_encoder_t enc;
|
|
857
|
-
nanocbor_encoder_init(&enc,
|
|
860
|
+
nanocbor_encoder_init(&enc, &measure_base, 0);
|
|
858
861
|
nanocbor_fmt_map(&enc, 2);
|
|
859
862
|
nanocbor_put_tstr(&enc, "prefix");
|
|
860
863
|
nanocbor_put_tstrn(&enc, partial, len);
|
|
@@ -904,8 +907,11 @@ static void refresh_ready(MIKReplTransport* transport);
|
|
|
904
907
|
* the length the encoding needs, which exceeds cap when it did not fit. */
|
|
905
908
|
static size_t encode_ready(uint8_t* buf, size_t cap, const char* chip, const char* id,
|
|
906
909
|
const char* version, const char* fw, const char* name) {
|
|
910
|
+
/* Measuring calls pass buf=NULL; substitute a non-null base so
|
|
911
|
+
* zero-length appends never hand memcpy a null pointer (UB). */
|
|
912
|
+
static uint8_t measure_base;
|
|
907
913
|
nanocbor_encoder_t enc;
|
|
908
|
-
nanocbor_encoder_init(&enc, buf, cap);
|
|
914
|
+
nanocbor_encoder_init(&enc, buf ? buf : &measure_base, buf ? cap : 0);
|
|
909
915
|
nanocbor_fmt_map(&enc, 3 + (fw ? 1 : 0) + (name ? 1 : 0));
|
|
910
916
|
nanocbor_put_tstr(&enc, "chip");
|
|
911
917
|
nanocbor_put_tstr(&enc, chip);
|