mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-09-23 00:36:15 +00:00
A built-in USB Files app (T-Deck and Heltec V4 for now) lets the page at files.wadamesh.com browse, upload, download, rename and delete files on the SD card, internal storage and the map tiles over Web Serial. File level, not USB mass storage: the device keeps its filesystems mounted, the radio keeps running, and the firmware enforces what may change. Live data (identity, contacts, settings, history; /meshcomod on the card) is download-only. - UsbFilesProtocol.h: framed messages (E7 5A, CRC32), a resyncing parser, path checks and the access policy; host tests in test/. - UsbFilesSession: one request at a time (both Arduino USB serial drivers drop bytes when their RX queue is full), RX queue sized per session, SD data through a DMA-capable bounce buffer, FatFs listings with sizes, a beacon so the page never talks first, SD Scan's malware verdict per file. - The companion link and MyMesh's console step off USB while it runs. - Website: labelled side buttons, a USB Files button and card; the files.wadamesh.com page, vhost and deploy script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
204 lines
8.1 KiB
C++
204 lines
8.1 KiB
C++
#include <assert.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "helpers/esp32/UsbFilesProtocol.h"
|
|
|
|
using namespace UsbFiles;
|
|
|
|
static uint8_t g_buf[2 * kMaxFrame];
|
|
|
|
static size_t frame(uint8_t* out, uint8_t type, uint16_t seq, const char* text) {
|
|
return encodeFrame(out, kMaxFrame, type, seq, reinterpret_cast<const uint8_t*>(text),
|
|
text ? strlen(text) : 0);
|
|
}
|
|
|
|
static bool pathOk(const char* p) { return pathValid(p, strlen(p)); }
|
|
|
|
static Access acc(Root r, const char* rel, bool ci) { return access(r, rel, ci); }
|
|
|
|
int main() {
|
|
// CRC is the IEEE one (check value of "123456789").
|
|
assert(crc32(reinterpret_cast<const uint8_t*>("123456789"), 9) == 0xCBF43926u);
|
|
|
|
// ---- round trip ----
|
|
uint8_t f[kMaxFrame];
|
|
size_t n = frame(f, T_HELLO, 7, "hi");
|
|
assert(n == kHeaderBytes + 2 + kTrailerBytes);
|
|
assert(f[0] == kMagic0 && f[1] == kMagic1 && f[2] == T_HELLO);
|
|
{
|
|
FrameParser p(g_buf, sizeof g_buf);
|
|
assert(p.push(f, n) == n);
|
|
assert(p.next());
|
|
assert(p.type() == T_HELLO && p.seq() == 7 && p.length() == 2);
|
|
assert(memcmp(p.payload(), "hi", 2) == 0);
|
|
assert(!p.next());
|
|
}
|
|
|
|
// ---- empty payload, and max payload ----
|
|
{
|
|
FrameParser p(g_buf, sizeof g_buf);
|
|
n = encodeFrame(f, sizeof f, T_PING, 1, nullptr, 0);
|
|
assert(n == kHeaderBytes + kTrailerBytes);
|
|
p.push(f, n);
|
|
assert(p.next() && p.type() == T_PING && p.length() == 0);
|
|
|
|
static uint8_t big[kMaxPayload];
|
|
for (size_t i = 0; i < sizeof big; ++i) big[i] = static_cast<uint8_t>(i * 7);
|
|
n = encodeFrame(f, sizeof f, T_WRITE_DATA, 2, big, sizeof big);
|
|
assert(n == kMaxFrame);
|
|
p.push(f, n);
|
|
assert(p.next() && p.length() == kMaxPayload && memcmp(p.payload(), big, sizeof big) == 0);
|
|
assert(encodeFrame(f, sizeof f, T_WRITE_DATA, 2, big, sizeof big + 1) == 0); // too long
|
|
assert(encodeFrame(f, n - 1, T_WRITE_DATA, 2, big, sizeof big) == 0); // no room
|
|
}
|
|
|
|
// ---- log text around frames, byte-at-a-time delivery ----
|
|
{
|
|
FrameParser p(g_buf, sizeof g_buf);
|
|
const char* noise = "[E][vfs_api.cpp:104] open(): /spiffs/x does not exist\r\n";
|
|
uint8_t stream[3 * kMaxFrame];
|
|
size_t s = 0;
|
|
memcpy(stream + s, noise, strlen(noise)); s += strlen(noise);
|
|
s += frame(stream + s, T_LIST, 10, "/sd");
|
|
memcpy(stream + s, noise, strlen(noise)); s += strlen(noise);
|
|
stream[s++] = kMagic0; // a stray half-magic
|
|
s += frame(stream + s, T_STAT, 11, "/internal");
|
|
int got = 0;
|
|
uint16_t seqs[2] = {0, 0};
|
|
for (size_t i = 0; i < s; ++i) {
|
|
p.push(stream + i, 1);
|
|
while (p.next()) seqs[got++] = p.seq();
|
|
}
|
|
assert(got == 2 && seqs[0] == 10 && seqs[1] == 11);
|
|
assert(p.droppedBytes() > 0);
|
|
}
|
|
|
|
// ---- a frame with a lost byte, then its retransmission: the retransmit is found ----
|
|
{
|
|
FrameParser p(g_buf, sizeof g_buf);
|
|
uint8_t a[kMaxFrame], b[kMaxFrame];
|
|
const size_t na = frame(a, T_READ, 20, "0123456789abcdef/some/path");
|
|
const size_t nb = frame(b, T_READ, 20, "0123456789abcdef/some/path");
|
|
p.push(a, 10);
|
|
p.push(a + 11, na - 11); // byte 10 lost
|
|
assert(!p.next()); // still waiting: the header claims more bytes
|
|
p.push(b, nb);
|
|
bool found = false;
|
|
while (p.next()) { found = (p.seq() == 20 && p.type() == T_READ); }
|
|
assert(found);
|
|
assert(p.badFrames() >= 1);
|
|
}
|
|
|
|
// ---- corrupted CRC is rejected, the next frame still parses ----
|
|
{
|
|
FrameParser p(g_buf, sizeof g_buf);
|
|
uint8_t a[kMaxFrame], b[kMaxFrame];
|
|
const size_t na = frame(a, T_MKDIR, 30, "/sd/x");
|
|
const size_t nb = frame(b, T_MKDIR, 31, "/sd/y");
|
|
a[na - 1] ^= 0xFF;
|
|
p.push(a, na);
|
|
p.push(b, nb);
|
|
assert(p.next() && p.seq() == 31);
|
|
assert(!p.next());
|
|
}
|
|
|
|
// ---- impossible length does not stall the parser ----
|
|
{
|
|
FrameParser p(g_buf, sizeof g_buf);
|
|
uint8_t junk[7] = {kMagic0, kMagic1, 1, 0, 0, 0xFF, 0xFF}; // len 65535
|
|
p.push(junk, sizeof junk);
|
|
n = frame(f, T_PING, 40, nullptr);
|
|
p.push(f, n);
|
|
assert(p.next() && p.seq() == 40);
|
|
}
|
|
|
|
// ---- paths ----
|
|
assert(pathOk("/"));
|
|
assert(pathOk("/sd"));
|
|
assert(pathOk("/sd/tiles/12/2100/1300.png"));
|
|
assert(pathOk("/internal/meshcore-2026.json"));
|
|
assert(pathOk("/sd/caf\xC3\xA9 notes.txt")); // UTF-8 and spaces are fine
|
|
assert(!pathOk(""));
|
|
assert(!pathOk("sd/x"));
|
|
assert(!pathOk("/sd/"));
|
|
assert(!pathOk("/sd//x"));
|
|
assert(!pathOk("/sd/./x"));
|
|
assert(!pathOk("/sd/../internal/identity"));
|
|
assert(!pathOk("/sd/.."));
|
|
assert(!pathOk("/sd/a\\b"));
|
|
assert(!pathOk("/sd/a:b"));
|
|
assert(!pathOk("/sd/a\x01" "b"));
|
|
assert(pathOk("/sd/..hidden")); // only exact "." and ".." components are special
|
|
{
|
|
char longp[kMaxPath + 2];
|
|
memset(longp, 'a', sizeof longp);
|
|
longp[0] = '/';
|
|
assert(pathValid(longp, kMaxPath));
|
|
assert(!pathValid(longp, kMaxPath + 1));
|
|
}
|
|
|
|
Root r;
|
|
char rel[kMaxPath + 1];
|
|
assert(splitPath("/", 1, &r, rel, sizeof rel) && r == ROOT_NONE && strcmp(rel, "/") == 0);
|
|
assert(splitPath("/sd", 3, &r, rel, sizeof rel) && r == ROOT_SD && strcmp(rel, "/") == 0);
|
|
assert(splitPath("/sd/a/b.txt", 11, &r, rel, sizeof rel) && r == ROOT_SD &&
|
|
strcmp(rel, "/a/b.txt") == 0);
|
|
assert(splitPath("/internal/x", 11, &r, rel, sizeof rel) && r == ROOT_INTERNAL &&
|
|
strcmp(rel, "/x") == 0);
|
|
assert(splitPath("/tiles/12", 9, &r, rel, sizeof rel) && r == ROOT_TILES &&
|
|
strcmp(rel, "/12") == 0);
|
|
assert(!splitPath("/sdx/a", 6, &r, rel, sizeof rel));
|
|
assert(!splitPath("/SD/a", 5, &r, rel, sizeof rel)); // root names are exact
|
|
assert(!splitPath("/nope", 5, &r, rel, sizeof rel));
|
|
|
|
// ---- access policy ----
|
|
// Roots themselves are read-only.
|
|
assert(acc(ROOT_SD, "/", true) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/", false) == A_READ);
|
|
// SD: everything but the firmware's data folder, including FAT's case folding
|
|
// and 8.3 aliases.
|
|
assert(acc(ROOT_SD, "/tiles/1/2/3.png", true) == A_WRITE);
|
|
assert(acc(ROOT_SD, "/transfer/a.bin", true) == A_WRITE);
|
|
assert(acc(ROOT_SD, "/meshcomod", true) == A_READ);
|
|
assert(acc(ROOT_SD, "/meshcomod/identity/_main.id", true) == A_READ);
|
|
assert(acc(ROOT_SD, "/MeshCoMod/contacts3", true) == A_READ);
|
|
assert(acc(ROOT_SD, "/MESHCO~1/contacts3", true) == A_READ);
|
|
assert(acc(ROOT_SD, "/meshcomodx", true) == A_WRITE); // a different name
|
|
// Internal: only the user folders and top-level backups / crash reports.
|
|
assert(acc(ROOT_INTERNAL, "/transfer", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/transfer/notes.txt", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/screenshots/s1.png", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/lock/wall.jpg", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/meshcore-2026-09-19.json", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/wadamesh-crash.elf", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/wadamesh-crash.elf.txt", false) == A_WRITE);
|
|
assert(acc(ROOT_INTERNAL, "/identity/_main.id", false) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/contacts3", false) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/new_prefs", false) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/prefs/touch.kv", false) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/msgs/seg_0001", false) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/apps/sdscan/main.lua", false) == A_READ);
|
|
assert(acc(ROOT_INTERNAL, "/sub/meshcore-x.json", false) == A_READ); // top level only
|
|
assert(acc(ROOT_INTERNAL, "/meshcore-.json", false) == A_READ); // too short to be one
|
|
assert(acc(ROOT_INTERNAL, "/Transfer/x", false) == A_READ); // SPIFFS is case-sensitive
|
|
assert(acc(ROOT_INTERNAL, "/Transfer/x", true) == A_WRITE); // FAT is not
|
|
// Tiles: the whole partition.
|
|
assert(acc(ROOT_TILES, "/12/2100/1300.png", false) == A_WRITE);
|
|
assert(acc(ROOT_NONE, "/x", false) == A_NONE);
|
|
|
|
// ---- JSON escaping ----
|
|
{
|
|
char out[64];
|
|
size_t pos = jsonAppendEscaped(out, 0, sizeof out, "a\"b\\c\n\xC3\xA9");
|
|
assert(strcmp(out, "a\\\"b\\\\c\\u000a\xC3\xA9") == 0);
|
|
assert(pos == strlen(out));
|
|
char tiny[4];
|
|
assert(jsonAppendEscaped(tiny, 0, sizeof tiny, "abcdef") == sizeof tiny);
|
|
}
|
|
|
|
printf("usb files protocol: all tests passed\n");
|
|
return 0;
|
|
}
|