@mikrojs/firmware 0.0.7

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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/idf.py +7 -0
  4. package/chips.json +3 -0
  5. package/cmake.js +9 -0
  6. package/components/mikrojs/CMakeLists.txt +187 -0
  7. package/components/mikrojs/Kconfig +55 -0
  8. package/components/mikrojs/idf_component.yml +6 -0
  9. package/components/mikrojs/include/mem.h +3 -0
  10. package/components/mikrojs/include/mik_color.h +3 -0
  11. package/components/mikrojs/include/mik_http_internal.h +77 -0
  12. package/components/mikrojs/include/mikrojs.h +5 -0
  13. package/components/mikrojs/include/mikrojs_esp32.h +65 -0
  14. package/components/mikrojs/include/private.h +10 -0
  15. package/components/mikrojs/include/utils.h +3 -0
  16. package/components/mikrojs/mik_ble.cpp +1588 -0
  17. package/components/mikrojs/mik_ble_c_shim.c +61 -0
  18. package/components/mikrojs/mik_ble_c_shim.h +37 -0
  19. package/components/mikrojs/mik_config.cpp +167 -0
  20. package/components/mikrojs/mik_deploy.cpp +584 -0
  21. package/components/mikrojs/mik_http.cpp +916 -0
  22. package/components/mikrojs/mik_i2c.cpp +364 -0
  23. package/components/mikrojs/mik_main.cpp +542 -0
  24. package/components/mikrojs/mik_neopixel.cpp +437 -0
  25. package/components/mikrojs/mik_nvs_kv.cpp +219 -0
  26. package/components/mikrojs/mik_pin.cpp +195 -0
  27. package/components/mikrojs/mik_pwm.cpp +525 -0
  28. package/components/mikrojs/mik_recovery.cpp +86 -0
  29. package/components/mikrojs/mik_rtc.cpp +305 -0
  30. package/components/mikrojs/mik_serial_io.cpp +362 -0
  31. package/components/mikrojs/mik_sleep.cpp +226 -0
  32. package/components/mikrojs/mik_sntp.cpp +275 -0
  33. package/components/mikrojs/mik_spi.cpp +330 -0
  34. package/components/mikrojs/mik_uart.cpp +497 -0
  35. package/components/mikrojs/mik_wifi.cpp +1434 -0
  36. package/components/mikrojs/platform_esp32.cpp +192 -0
  37. package/components/mikrojs/test/CMakeLists.txt +32 -0
  38. package/components/mikrojs/test/abort_test.cpp +254 -0
  39. package/components/mikrojs/test/ble_test.cpp +714 -0
  40. package/components/mikrojs/test/fs_js_test.cpp +458 -0
  41. package/components/mikrojs/test/fs_pub_test.cpp +312 -0
  42. package/components/mikrojs/test/http_test.cpp +475 -0
  43. package/components/mikrojs/test/i2c_test.cpp +138 -0
  44. package/components/mikrojs/test/modules_extended_test.cpp +137 -0
  45. package/components/mikrojs/test/modules_test.cpp +131 -0
  46. package/components/mikrojs/test/pins_test.cpp +47 -0
  47. package/components/mikrojs/test/pwm_test.cpp +166 -0
  48. package/components/mikrojs/test/repl_protocol_test.cpp +405 -0
  49. package/components/mikrojs/test/rtc_test.cpp +331 -0
  50. package/components/mikrojs/test/runtime_test.cpp +89 -0
  51. package/components/mikrojs/test/sleep_test.cpp +222 -0
  52. package/components/mikrojs/test/sntp_test.cpp +249 -0
  53. package/components/mikrojs/test/stdio_test.cpp +449 -0
  54. package/components/mikrojs/test/sys_test.cpp +165 -0
  55. package/components/mikrojs/test/text_encoding_test.cpp +224 -0
  56. package/components/mikrojs/test/timers_js_test.cpp +244 -0
  57. package/components/mikrojs/test/timers_test.cpp +79 -0
  58. package/components/mikrojs/test/wifi_test.cpp +599 -0
  59. package/default-app/main/CMakeLists.txt +3 -0
  60. package/default-app/main/main.cpp +5 -0
  61. package/discover.js +77 -0
  62. package/index.d.ts +7 -0
  63. package/index.js +20 -0
  64. package/package.json +61 -0
  65. package/partitions.csv +5 -0
  66. package/prebuilds/esp32/bootloader/bootloader.bin +0 -0
  67. package/prebuilds/esp32/flasher_args.json +24 -0
  68. package/prebuilds/esp32/mikrojs.bin +0 -0
  69. package/prebuilds/esp32/partition_table/partition-table.bin +0 -0
  70. package/prebuilds/esp32c3/bootloader/bootloader.bin +0 -0
  71. package/prebuilds/esp32c3/flasher_args.json +24 -0
  72. package/prebuilds/esp32c3/mikrojs.bin +0 -0
  73. package/prebuilds/esp32c3/partition_table/partition-table.bin +0 -0
  74. package/prebuilds/esp32c6/bootloader/bootloader.bin +0 -0
  75. package/prebuilds/esp32c6/flasher_args.json +24 -0
  76. package/prebuilds/esp32c6/mikrojs.bin +0 -0
  77. package/prebuilds/esp32c6/partition_table/partition-table.bin +0 -0
  78. package/prebuilds/esp32s3/bootloader/bootloader.bin +0 -0
  79. package/prebuilds/esp32s3/flasher_args.json +24 -0
  80. package/prebuilds/esp32s3/mikrojs.bin +0 -0
  81. package/prebuilds/esp32s3/partition_table/partition-table.bin +0 -0
  82. package/project.cmake +101 -0
  83. package/resolve.js +54 -0
  84. package/sdkconfig.defaults +127 -0
  85. package/sdkconfig.defaults.esp32 +8 -0
  86. package/sdkconfig.defaults.esp32c3 +15 -0
  87. package/sdkconfig.defaults.esp32c6 +26 -0
  88. package/sdkconfig.defaults.esp32s3 +22 -0
@@ -0,0 +1,584 @@
1
+ #include <dirent.h>
2
+ #include <errno.h>
3
+ #include <stdio.h>
4
+ #include <string.h>
5
+ #include <sys/stat.h>
6
+ #include <unistd.h>
7
+
8
+ #include <esp_app_desc.h>
9
+ #include <freertos/FreeRTOS.h>
10
+ #include <freertos/task.h>
11
+
12
+ #include "mikrojs/mikrojs.h"
13
+ #include "mikrojs/platform.h"
14
+ #include "mikrojs/private.h"
15
+ #include "mikrojs_esp32.h"
16
+
17
+ static const char* FS_BASE = "/appfs";
18
+ static const char* DEPLOY_TMP = "/appfs/.deploy-tmp";
19
+ static const char* DEPLOY_OLD = "/appfs/.deploy-old";
20
+ static const char* APP_DIR = "/appfs/app";
21
+ static const char* CHECKSUMS_PATH = "/appfs/app/.checksums";
22
+
23
+ /* ── Checksums manifest ─────────────────────────────────────────── */
24
+
25
+ /*
26
+ * Text format (sha256sum-style), one entry per line:
27
+ * <64-char hex hash> <filename>\n
28
+ * The device appends a "#firmware:<elf_sha256>" line after each deploy.
29
+ * On load, if the firmware hash doesn't match, the manifest is discarded.
30
+ */
31
+
32
+ static constexpr const char* FIRMWARE_PREFIX = "#firmware:";
33
+ static constexpr size_t FIRMWARE_PREFIX_LEN = 10;
34
+
35
+ struct ChecksumsManifest {
36
+ char* data;
37
+ size_t len;
38
+ };
39
+
40
+ static ChecksumsManifest load_checksums_manifest() {
41
+ ChecksumsManifest m = {nullptr, 0};
42
+
43
+ FILE* f = fopen(CHECKSUMS_PATH, "r");
44
+ if (!f) return m;
45
+
46
+ fseek(f, 0, SEEK_END);
47
+ long size = ftell(f);
48
+ fseek(f, 0, SEEK_SET);
49
+
50
+ if (size <= 0) {
51
+ fclose(f);
52
+ return m;
53
+ }
54
+
55
+ auto* buf = static_cast<char*>(malloc(size + 1));
56
+ if (!buf) {
57
+ fclose(f);
58
+ return m;
59
+ }
60
+
61
+ size_t n = fread(buf, 1, size, f);
62
+ fclose(f);
63
+ buf[n] = '\0';
64
+
65
+ /* Check firmware hash — if it doesn't match, discard the manifest */
66
+ const esp_app_desc_t* app_desc = esp_app_get_description();
67
+ char fw_hash[65];
68
+ for (int i = 0; i < 32; i++) {
69
+ snprintf(fw_hash + i * 2, 3, "%02x", app_desc->app_elf_sha256[i]);
70
+ }
71
+ bool fw_match = false;
72
+ const char* p = buf;
73
+ const char* end = buf + n;
74
+ while (p < end) {
75
+ const char* nl = static_cast<const char*>(memchr(p, '\n', end - p));
76
+ size_t line_len = nl ? static_cast<size_t>(nl - p) : static_cast<size_t>(end - p);
77
+ if (line_len > FIRMWARE_PREFIX_LEN &&
78
+ memcmp(p, FIRMWARE_PREFIX, FIRMWARE_PREFIX_LEN) == 0) {
79
+ const char* stored = p + FIRMWARE_PREFIX_LEN;
80
+ size_t hash_len = line_len - FIRMWARE_PREFIX_LEN;
81
+ if (hash_len == strlen(fw_hash) && memcmp(stored, fw_hash, hash_len) == 0) {
82
+ fw_match = true;
83
+ }
84
+ break;
85
+ }
86
+ p = nl ? nl + 1 : end;
87
+ }
88
+
89
+ if (!fw_match) {
90
+ free(buf);
91
+ return m;
92
+ }
93
+
94
+ m.data = buf;
95
+ m.len = n;
96
+ return m;
97
+ }
98
+
99
+ /**
100
+ * Append the current firmware hash to the checksums manifest so it can
101
+ * be validated on next load.
102
+ */
103
+ static void stamp_checksums_manifest() {
104
+ FILE* f = fopen(CHECKSUMS_PATH, "a");
105
+ if (!f) return;
106
+ const esp_app_desc_t* desc = esp_app_get_description();
107
+ char hash[65];
108
+ for (int i = 0; i < 32; i++) {
109
+ snprintf(hash + i * 2, 3, "%02x", desc->app_elf_sha256[i]);
110
+ }
111
+ fprintf(f, "%s%s\n", FIRMWARE_PREFIX, hash);
112
+ fclose(f);
113
+ }
114
+
115
+ static void hash_to_hex(const uint8_t* hash, char* out) {
116
+ static const char hex[] = "0123456789abcdef";
117
+ for (int i = 0; i < 32; i++) {
118
+ out[i * 2] = hex[hash[i] >> 4];
119
+ out[i * 2 + 1] = hex[hash[i] & 0x0f];
120
+ }
121
+ out[64] = '\0';
122
+ }
123
+
124
+ /**
125
+ * Look up a filename in the text manifest. Returns true if the given
126
+ * 32-byte hash matches the stored hex hash for that filename.
127
+ */
128
+ static bool manifest_matches(const ChecksumsManifest& m, const char* name, uint16_t name_len,
129
+ const uint8_t* hash) {
130
+ if (!m.data) return false;
131
+
132
+ char hex[65];
133
+ hash_to_hex(hash, hex);
134
+
135
+ /* Scan lines: each is "<64 hex> <filename>\n" */
136
+ const char* p = m.data;
137
+ const char* end = m.data + m.len;
138
+ while (p < end) {
139
+ const char* nl = static_cast<const char*>(memchr(p, '\n', end - p));
140
+ size_t line_len = nl ? static_cast<size_t>(nl - p) : static_cast<size_t>(end - p);
141
+
142
+ /* Line must be at least 64 (hash) + 2 (" ") + 1 (filename) */
143
+ if (line_len >= 67) {
144
+ /* Compare hex hash (first 64 chars) */
145
+ if (memcmp(p, hex, 64) == 0 && p[64] == ' ' && p[65] == ' ') {
146
+ /* Compare filename */
147
+ const char* fname = p + 66;
148
+ size_t fname_len = line_len - 66;
149
+ if (fname_len == name_len && memcmp(fname, name, name_len) == 0) {
150
+ return true;
151
+ }
152
+ }
153
+ }
154
+
155
+ p = nl ? nl + 1 : end;
156
+ }
157
+ return false;
158
+ }
159
+
160
+ /* ── Filesystem helpers ──────────────────────────────────────────── */
161
+
162
+ static void mkdirs(const char* path) {
163
+ char tmp[512];
164
+ snprintf(tmp, sizeof(tmp), "%s", path);
165
+ for (char* p = tmp + 1; *p; p++) {
166
+ if (*p == '/') {
167
+ *p = '\0';
168
+ mkdir(tmp, 0755);
169
+ *p = '/';
170
+ }
171
+ }
172
+ mkdir(tmp, 0755);
173
+ }
174
+
175
+ static void rmdir_recursive(const char* path) {
176
+ DIR* dir = opendir(path);
177
+ if (!dir) return;
178
+
179
+ struct dirent* entry;
180
+ while ((entry = readdir(dir)) != NULL) {
181
+ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
182
+
183
+ char full[512];
184
+ snprintf(full, sizeof(full), "%s/%s", path, entry->d_name);
185
+
186
+ struct stat st;
187
+ if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) {
188
+ rmdir_recursive(full);
189
+ } else {
190
+ unlink(full);
191
+ }
192
+ }
193
+ closedir(dir);
194
+ rmdir(path);
195
+ }
196
+
197
+ static bool path_exists(const char* path) {
198
+ struct stat st;
199
+ return stat(path, &st) == 0;
200
+ }
201
+
202
+ /* ── File copy helper ────────────────────────────────────────────── */
203
+
204
+ static bool copy_file(const char* src, const char* dst) {
205
+ FILE* in = fopen(src, "r");
206
+ if (!in) return false;
207
+
208
+ FILE* out = fopen(dst, "w");
209
+ if (!out) {
210
+ int saved = errno;
211
+ fclose(in);
212
+ errno = saved;
213
+ return false;
214
+ }
215
+
216
+ uint8_t buf[512];
217
+ size_t n;
218
+ bool ok = true;
219
+ int saved_errno = 0;
220
+ while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
221
+ if (fwrite(buf, 1, n, out) != n) {
222
+ saved_errno = errno;
223
+ ok = false;
224
+ break;
225
+ }
226
+ }
227
+ if (ok && ferror(in)) {
228
+ saved_errno = errno;
229
+ ok = false;
230
+ }
231
+
232
+ fflush(out);
233
+ fclose(out);
234
+ fclose(in);
235
+ if (!ok) errno = saved_errno;
236
+ return ok;
237
+ }
238
+
239
+ /* ── Deploy recovery ─────────────────────────────────────────────── */
240
+
241
+ void MIK_DeployRecover(void) {
242
+ bool has_app = path_exists(APP_DIR);
243
+ bool has_old = path_exists(DEPLOY_OLD);
244
+ bool has_tmp = path_exists(DEPLOY_TMP);
245
+
246
+ if (!has_app && has_old) {
247
+ rename(DEPLOY_OLD, APP_DIR);
248
+ } else if (has_old) {
249
+ rmdir_recursive(DEPLOY_OLD);
250
+ }
251
+
252
+ if (has_tmp) {
253
+ rmdir_recursive(DEPLOY_TMP);
254
+ }
255
+ }
256
+
257
+ /* ── Unified protocol deploy handler ────────────────────────────── */
258
+
259
+ /* Deploy state — persists across multiple deploy commands within one session.
260
+ * Lazily initialized on the first deploy command. */
261
+ static bool s_deploy_active = false;
262
+ static bool s_deploy_erased = false;
263
+ static bool s_deploy_prev_paused = false;
264
+ static ChecksumsManifest s_deploy_manifest = {nullptr, 0};
265
+
266
+ /* In-progress streaming PUT. Set by CMD_DEPLOY_PUT (open file + record size),
267
+ * appended to by CMD_DEPLOY_PUT_CHUNK, cleared when total_remaining hits zero
268
+ * or when the deploy session is reset. f is non-null iff a PUT is currently
269
+ * receiving body bytes. */
270
+ static FILE* s_put_file = nullptr;
271
+ static uint32_t s_put_remaining = 0;
272
+
273
+ static void put_close_and_clear() {
274
+ if (s_put_file) {
275
+ fflush(s_put_file);
276
+ fclose(s_put_file);
277
+ s_put_file = nullptr;
278
+ }
279
+ s_put_remaining = 0;
280
+ }
281
+
282
+ static void deploy_ensure_init() {
283
+ if (s_deploy_active) return;
284
+ s_deploy_active = true;
285
+ s_deploy_erased = false;
286
+ /* Freeze user code for the duration of the deploy: timers and loop
287
+ * consumers stop firing between protocol commands so an app crash
288
+ * (or a WiFi/HTTP callback touching the fs) can't race the file
289
+ * swap. Microtasks still drain. Prior state is restored on cleanup
290
+ * so a manual /pause sticks across a deploy. */
291
+ s_deploy_prev_paused = mik__repl_is_paused();
292
+ mik__repl_set_paused(true);
293
+ rmdir_recursive(DEPLOY_TMP);
294
+ s_deploy_manifest = load_checksums_manifest();
295
+ }
296
+
297
+ static void deploy_cleanup() {
298
+ put_close_and_clear();
299
+ if (s_deploy_manifest.data) {
300
+ free(s_deploy_manifest.data);
301
+ s_deploy_manifest = {nullptr, 0};
302
+ }
303
+ mik__repl_set_paused(s_deploy_prev_paused);
304
+ s_deploy_active = false;
305
+ }
306
+
307
+ /**
308
+ * Reset any in-flight deploy session. Called from the REPL protocol
309
+ * session_end hook when the transport drops without a DONE/ABORT (e.g.
310
+ * CLI killed mid-upload). Discards the staging dir so the next session
311
+ * starts from a clean slate and restores the prior pause state.
312
+ */
313
+ void mik__deploy_session_reset(void) {
314
+ if (!s_deploy_active) return;
315
+ rmdir_recursive(DEPLOY_TMP);
316
+ deploy_cleanup();
317
+ }
318
+
319
+ /**
320
+ * Handle a deploy command from the unified protocol loop.
321
+ * The payload bytes have NOT been read yet — this handler reads them
322
+ * directly from the transport, enabling streaming for large files.
323
+ */
324
+ bool mik__handle_deploy_command(MIKReplTransport* transport, uint8_t cmd_type,
325
+ uint32_t payload_len) {
326
+ deploy_ensure_init();
327
+
328
+ switch (cmd_type) {
329
+ case MIK_CMD_DEPLOY_CHECKSUM: {
330
+ /* Payload: u16le name_len | name | sha256[32] */
331
+ uint8_t hdr[2];
332
+ if (!mik__proto_read_exact(transport, hdr, 2)) return false;
333
+ uint16_t name_len = hdr[0] | (hdr[1] << 8);
334
+
335
+ char name[256];
336
+ if (name_len >= sizeof(name)) {
337
+ mik__proto_drain(transport, payload_len - 2);
338
+ mik__proto_send_err(transport, "checksum filename too long");
339
+ return true;
340
+ }
341
+ if (!mik__proto_read_exact(transport, name, name_len)) return false;
342
+
343
+ uint8_t hash[32];
344
+ if (!mik__proto_read_exact(transport, hash, 32)) return false;
345
+
346
+ bool match = manifest_matches(s_deploy_manifest, name, name_len, hash);
347
+
348
+ /* Verify the file is actually on disk. A stale manifest (e.g. after
349
+ * a partial deploy or FS glitch) would otherwise trick the client
350
+ * into sending KEEP for a file that no longer exists, and KEEP
351
+ * would then fail. Report "no match" so the client re-PUTs it. */
352
+ if (match) {
353
+ char terminated[257];
354
+ memcpy(terminated, name, name_len);
355
+ terminated[name_len] = '\0';
356
+ char src_path[512];
357
+ snprintf(src_path, sizeof(src_path), "%s%s", FS_BASE, terminated);
358
+ if (!path_exists(src_path)) match = false;
359
+ }
360
+
361
+ uint8_t result_byte = match ? 0x01 : 0x00;
362
+ mik__proto_send(transport, MIK_MSG_CHECKSUM_RESULT, &result_byte, 1);
363
+ return true;
364
+ }
365
+
366
+ case MIK_CMD_DEPLOY_ERASE:
367
+ mik__proto_drain(transport, payload_len);
368
+ rmdir_recursive(APP_DIR);
369
+ s_deploy_erased = true;
370
+ mik__proto_send_ok(transport);
371
+ return true;
372
+
373
+ case MIK_CMD_DEPLOY_KEEP: {
374
+ /* Payload: u16le name_len | name */
375
+ uint8_t nl[2];
376
+ if (!mik__proto_read_exact(transport, nl, 2)) return false;
377
+ uint16_t name_len = nl[0] | (nl[1] << 8);
378
+
379
+ char name[256];
380
+ if (name_len >= sizeof(name)) {
381
+ mik__proto_drain(transport, name_len);
382
+ mik__proto_send_err(transport, "keep filename too long");
383
+ return true;
384
+ }
385
+ if (!mik__proto_read_exact(transport, name, name_len)) return false;
386
+ name[name_len] = '\0';
387
+
388
+ char src_path[512], dst_path[512];
389
+ snprintf(src_path, sizeof(src_path), "%s%s", FS_BASE, name);
390
+ snprintf(dst_path, sizeof(dst_path), "%s%s", DEPLOY_TMP, name);
391
+
392
+ char dir_path[512];
393
+ snprintf(dir_path, sizeof(dir_path), "%s", dst_path);
394
+ char* last_slash = strrchr(dir_path, '/');
395
+ if (last_slash && last_slash != dir_path) {
396
+ *last_slash = '\0';
397
+ mkdirs(dir_path);
398
+ }
399
+
400
+ if (copy_file(src_path, dst_path)) {
401
+ mik__proto_send_ok(transport);
402
+ } else {
403
+ char msg[128];
404
+ snprintf(msg, sizeof(msg), "keep copy failed: %s", strerror(errno));
405
+ mik__proto_send_err(transport, msg);
406
+ }
407
+ return true;
408
+ }
409
+
410
+ case MIK_CMD_DEPLOY_PUT: {
411
+ /* Begin a streaming PUT.
412
+ * Payload: u16le name_len | name | u32le total_size
413
+ * Opens the staged file, records the expected body size, and
414
+ * replies OK. The body itself arrives in subsequent
415
+ * CMD_DEPLOY_PUT_CHUNK frames. */
416
+ put_close_and_clear();
417
+
418
+ if (payload_len < 2 + 4) {
419
+ mik__proto_drain(transport, payload_len);
420
+ mik__proto_send_err(transport, "put header too short");
421
+ return true;
422
+ }
423
+
424
+ uint8_t nl[2];
425
+ if (!mik__proto_read_exact(transport, nl, 2)) return false;
426
+ uint16_t name_len = nl[0] | (nl[1] << 8);
427
+
428
+ if (name_len >= 256 || (uint32_t)name_len + 6 > payload_len) {
429
+ mik__proto_drain(transport, payload_len - 2);
430
+ mik__proto_send_err(transport, "put filename too long");
431
+ return true;
432
+ }
433
+
434
+ char name[256];
435
+ if (!mik__proto_read_exact(transport, name, name_len)) return false;
436
+ name[name_len] = '\0';
437
+
438
+ uint8_t sz[4];
439
+ if (!mik__proto_read_exact(transport, sz, 4)) return false;
440
+ uint32_t total_size = (uint32_t)sz[0] | ((uint32_t)sz[1] << 8) |
441
+ ((uint32_t)sz[2] << 16) | ((uint32_t)sz[3] << 24);
442
+
443
+ /* Drain any trailing bytes the host accidentally tacked on
444
+ * (defensive — the wire format is fixed-size after the name). */
445
+ uint32_t consumed = 2 + name_len + 4;
446
+ if (payload_len > consumed) {
447
+ mik__proto_drain(transport, payload_len - consumed);
448
+ }
449
+
450
+ char full_path[512];
451
+ snprintf(full_path, sizeof(full_path), "%s%s", DEPLOY_TMP, name);
452
+
453
+ char dir_path[512];
454
+ snprintf(dir_path, sizeof(dir_path), "%s", full_path);
455
+ char* last_slash = strrchr(dir_path, '/');
456
+ if (last_slash && last_slash != dir_path) {
457
+ *last_slash = '\0';
458
+ mkdirs(dir_path);
459
+ }
460
+
461
+ FILE* f = fopen(full_path, "w");
462
+ if (!f) {
463
+ mik__proto_send_err(transport, "open failed");
464
+ return true;
465
+ }
466
+
467
+ if (total_size == 0) {
468
+ /* Empty file: nothing to chunk, close now. */
469
+ fflush(f);
470
+ fclose(f);
471
+ } else {
472
+ s_put_file = f;
473
+ s_put_remaining = total_size;
474
+ }
475
+ mik__proto_send_ok(transport);
476
+ return true;
477
+ }
478
+
479
+ case MIK_CMD_DEPLOY_PUT_CHUNK: {
480
+ /* Append payload bytes to the file opened by the preceding PUT.
481
+ * Streams in a small fixed buffer — never holds the chunk in
482
+ * full, regardless of chunk size. Closes the file and clears
483
+ * state when total_remaining reaches zero. */
484
+ if (!s_put_file) {
485
+ mik__proto_drain(transport, payload_len);
486
+ mik__proto_send_err(transport, "put chunk without active put");
487
+ return true;
488
+ }
489
+ if (payload_len > s_put_remaining) {
490
+ mik__proto_drain(transport, payload_len);
491
+ put_close_and_clear();
492
+ mik__proto_send_err(transport, "put chunk overruns declared size");
493
+ return true;
494
+ }
495
+
496
+ uint8_t buf[512];
497
+ uint32_t remaining = payload_len;
498
+ bool ok = true;
499
+ while (remaining > 0) {
500
+ uint32_t chunk = remaining > sizeof(buf) ? sizeof(buf) : remaining;
501
+ if (!mik__proto_read_exact(transport, buf, chunk)) return false;
502
+ if (fwrite(buf, 1, chunk, s_put_file) != chunk) {
503
+ /* Write failure: keep the protocol in sync by draining
504
+ * the rest of this chunk's bytes, then abort the PUT. */
505
+ remaining -= chunk;
506
+ if (remaining > 0) mik__proto_drain(transport, remaining);
507
+ ok = false;
508
+ break;
509
+ }
510
+ remaining -= chunk;
511
+ s_put_remaining -= chunk;
512
+ }
513
+
514
+ if (!ok) {
515
+ put_close_and_clear();
516
+ mik__proto_send_err(transport, "write failed");
517
+ return true;
518
+ }
519
+
520
+ if (s_put_remaining == 0) {
521
+ put_close_and_clear();
522
+ }
523
+ mik__proto_send_ok(transport);
524
+ return true;
525
+ }
526
+
527
+ case MIK_CMD_DEPLOY_DONE: {
528
+ mik__proto_drain(transport, payload_len);
529
+
530
+ /* Guard against a CLI bug where DONE arrives mid-stream: the
531
+ * half-written file would otherwise be promoted into APP_DIR.
532
+ * Drop the in-flight file, nuke staging, and surface the error
533
+ * to the host so the caller sees a real failure. */
534
+ if (s_put_file) {
535
+ put_close_and_clear();
536
+ rmdir_recursive(DEPLOY_TMP);
537
+ mik__proto_send_err(transport, "deploy done while put in progress");
538
+ deploy_cleanup();
539
+ return true;
540
+ }
541
+
542
+ /* Atomic swap: staging dir → app dir */
543
+ if (path_exists(APP_DIR) && !s_deploy_erased) {
544
+ rmdir_recursive(DEPLOY_OLD);
545
+ if (rename(APP_DIR, DEPLOY_OLD) != 0) {
546
+ mik__proto_send_err(transport, "rename app to old failed");
547
+ deploy_cleanup();
548
+ return true;
549
+ }
550
+ }
551
+
552
+ char staged_app[512];
553
+ snprintf(staged_app, sizeof(staged_app), "%s/app", DEPLOY_TMP);
554
+ if (path_exists(staged_app)) {
555
+ if (rename(staged_app, APP_DIR) != 0) {
556
+ if (path_exists(DEPLOY_OLD)) {
557
+ rename(DEPLOY_OLD, APP_DIR);
558
+ }
559
+ mik__proto_send_err(transport, "rename staged to app failed");
560
+ deploy_cleanup();
561
+ return true;
562
+ }
563
+ }
564
+
565
+ rmdir_recursive(DEPLOY_OLD);
566
+ rmdir_recursive(DEPLOY_TMP);
567
+ stamp_checksums_manifest();
568
+ mik__proto_send_ok(transport);
569
+ deploy_cleanup();
570
+ return true;
571
+ }
572
+
573
+ case MIK_CMD_DEPLOY_ABORT:
574
+ mik__proto_drain(transport, payload_len);
575
+ rmdir_recursive(DEPLOY_TMP);
576
+ mik__proto_send_ok(transport);
577
+ deploy_cleanup();
578
+ return true;
579
+
580
+ default:
581
+ mik__proto_drain(transport, payload_len);
582
+ return false;
583
+ }
584
+ }