#include "rustscript_loader.h"
#include <Arduino.h>
#include <SD.h>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include "esp_partition.h"
#include "rustscript_embedded.h"
#include "rustscript_framework.h"
#ifndef RUSTSCRIPT_SD_CS
#define RUSTSCRIPT_SD_CS 7
#endif
#ifndef RUSTSCRIPT_SD_PATH
#define RUSTSCRIPT_SD_PATH "/rustscript/main.vmbc"
#endif
namespace {
constexpr char SCRIPT_PARTITION_LABEL[] = "rustscript";
constexpr uint8_t SCRIPT_PARTITION_TYPE = 0x40;
constexpr uint32_t SCRIPT_IMAGE_VERSION = 1;
constexpr uint8_t SCRIPT_IMAGE_MAGIC[8] = {'R', 'S', 'S', 'V', 'M', 'B', 'C', 0};
constexpr size_t MAX_REPL_LINE = 128;
struct __attribute__((packed)) ScriptImageHeader {
uint8_t magic[8];
uint32_t version;
uint32_t payload_len;
uint32_t crc32;
};
const esp_partition_t *script_partition() {
return esp_partition_find_first(
static_cast<esp_partition_type_t>(SCRIPT_PARTITION_TYPE),
ESP_PARTITION_SUBTYPE_ANY,
SCRIPT_PARTITION_LABEL
);
}
bool header_valid(const ScriptImageHeader &header, size_t partition_size) {
return std::memcmp(header.magic, SCRIPT_IMAGE_MAGIC, sizeof(SCRIPT_IMAGE_MAGIC)) == 0 &&
header.version == SCRIPT_IMAGE_VERSION && header.payload_len != 0 &&
header.payload_len <= partition_size - sizeof(ScriptImageHeader);
}
bool read_serial_payload(uint8_t *data, size_t len) {
size_t offset = 0;
const unsigned long previous_timeout = Serial.getTimeout();
Serial.setTimeout(10000);
while (offset < len) {
const size_t count = Serial.readBytes(data + offset, len - offset);
if (count == 0) {
Serial.setTimeout(previous_timeout);
return false;
}
offset += count;
}
Serial.setTimeout(previous_timeout);
return true;
}
void run_repl_payload(const uint8_t *data, size_t len) {
const int32_t status = rustscript_run_vmbc(
data,
len,
rustscript_dispatch_host,
nullptr,
1000000
);
Serial.print("rss:status=");
Serial.println(status);
}
void print_repl_help() {
Serial.println("rss:VMBC REPL ready");
Serial.println("rss:load <length> <crc32-hex> run one VMBC payload");
Serial.println("rss:install <length> <crc32-hex> write and run the script partition");
Serial.println("rss:run run the script partition");
Serial.println("rss:info show partition information");
Serial.println("rss:help");
}
bool parse_transfer_command(
const String &line,
const char *command,
size_t *length,
uint32_t *expected_crc
) {
const size_t prefix_len = std::strlen(command);
if (!line.startsWith(command) || line.length() <= prefix_len || line[prefix_len] != ' ') {
return false;
}
const char *input = line.c_str() + prefix_len + 1;
char *end = nullptr;
const unsigned long parsed_len = std::strtoul(input, &end, 10);
if (end == input || *end != ' ' || parsed_len == 0) {
return false;
}
input = end + 1;
const unsigned long parsed_crc = std::strtoul(input, &end, 16);
if (end == input || *end != '\0') {
return false;
}
*length = static_cast<size_t>(parsed_len);
*expected_crc = static_cast<uint32_t>(parsed_crc);
return true;
}
void receive_repl_payload(size_t len, uint32_t expected_crc, bool install) {
const esp_partition_t *partition = script_partition();
const size_t maximum = partition == nullptr
? 0
: partition->size - sizeof(ScriptImageHeader);
if (len > maximum) {
Serial.println("rss:error=payload-too-large");
return;
}
auto *payload = static_cast<uint8_t *>(std::malloc(len));
if (payload == nullptr) {
Serial.println("rss:error=allocation-failed");
return;
}
Serial.println("rss:ready");
if (!read_serial_payload(payload, len)) {
Serial.println("rss:error=transfer-timeout");
std::free(payload);
return;
}
const uint32_t actual_crc = rustscript_crc32(payload, len);
if (actual_crc != expected_crc) {
Serial.print("rss:error=crc-mismatch actual=");
Serial.println(actual_crc, HEX);
std::free(payload);
return;
}
if (install && !rustscript_install_partition(payload, len)) {
Serial.println("rss:error=partition-write-failed");
std::free(payload);
return;
}
Serial.println(install ? "rss:installed" : "rss:loaded");
run_repl_payload(payload, len);
std::free(payload);
}
}
uint32_t rustscript_crc32(const uint8_t *data, size_t len) {
uint32_t crc = 0xffffffffU;
for (size_t index = 0; index < len; ++index) {
crc ^= data[index];
for (uint8_t bit = 0; bit < 8; ++bit) {
const uint32_t mask = 0U - (crc & 1U);
crc = (crc >> 1U) ^ (0xedb88320U & mask);
}
}
return ~crc;
}
bool rustscript_load_sd(RustScriptImage *image) {
if (image == nullptr || !SD.begin(RUSTSCRIPT_SD_CS)) {
return false;
}
File file = SD.open(RUSTSCRIPT_SD_PATH, FILE_READ);
if (!file || file.isDirectory() || file.size() == 0) {
if (file) {
file.close();
}
SD.end();
return false;
}
const size_t len = static_cast<size_t>(file.size());
auto *data = static_cast<uint8_t *>(std::malloc(len));
if (data == nullptr) {
file.close();
SD.end();
return false;
}
const size_t count = file.read(data, len);
file.close();
SD.end();
if (count != len) {
std::free(data);
return false;
}
image->data = data;
image->len = len;
image->source = "sd:/rustscript/main.vmbc";
return true;
}
bool rustscript_load_partition(RustScriptImage *image) {
if (image == nullptr) {
return false;
}
const esp_partition_t *partition = script_partition();
if (partition == nullptr || partition->size <= sizeof(ScriptImageHeader)) {
return false;
}
ScriptImageHeader header{};
if (esp_partition_read(partition, 0, &header, sizeof(header)) != ESP_OK ||
!header_valid(header, partition->size)) {
return false;
}
auto *data = static_cast<uint8_t *>(std::malloc(header.payload_len));
if (data == nullptr) {
return false;
}
if (esp_partition_read(partition, sizeof(header), data, header.payload_len) != ESP_OK ||
rustscript_crc32(data, header.payload_len) != header.crc32) {
std::free(data);
return false;
}
image->data = data;
image->len = header.payload_len;
image->source = "flash:rustscript";
return true;
}
bool rustscript_install_partition(const uint8_t *data, size_t len) {
const esp_partition_t *partition = script_partition();
if (partition == nullptr || data == nullptr || len == 0 ||
len > partition->size - sizeof(ScriptImageHeader)) {
return false;
}
ScriptImageHeader header{};
std::memcpy(header.magic, SCRIPT_IMAGE_MAGIC, sizeof(SCRIPT_IMAGE_MAGIC));
header.version = SCRIPT_IMAGE_VERSION;
header.payload_len = static_cast<uint32_t>(len);
header.crc32 = rustscript_crc32(data, len);
return esp_partition_erase_range(partition, 0, partition->size) == ESP_OK &&
esp_partition_write(partition, 0, &header, sizeof(header)) == ESP_OK &&
esp_partition_write(partition, sizeof(header), data, len) == ESP_OK;
}
void rustscript_free_image(RustScriptImage *image) {
if (image == nullptr) {
return;
}
std::free(image->data);
image->data = nullptr;
image->len = 0;
image->source = nullptr;
}
void rustscript_repl() {
print_repl_help();
while (true) {
if (!Serial.available()) {
delay(10);
continue;
}
String line = Serial.readStringUntil('\n');
line.trim();
if (line.length() == 0 || line.length() > MAX_REPL_LINE) {
continue;
}
size_t len = 0;
uint32_t crc = 0;
if (parse_transfer_command(line, "load", &len, &crc)) {
receive_repl_payload(len, crc, false);
} else if (parse_transfer_command(line, "install", &len, &crc)) {
receive_repl_payload(len, crc, true);
} else if (line == "run") {
RustScriptImage image{};
if (rustscript_load_partition(&image)) {
run_repl_payload(image.data, image.len);
rustscript_free_image(&image);
} else {
Serial.println("rss:error=no-partition-script");
}
} else if (line == "info") {
const esp_partition_t *partition = script_partition();
if (partition == nullptr) {
Serial.println("rss:partition=missing");
} else {
Serial.printf(
"rss:partition offset=0x%lx size=%lu\n",
static_cast<unsigned long>(partition->address),
static_cast<unsigned long>(partition->size)
);
}
} else if (line == "help") {
print_repl_help();
} else {
Serial.println("rss:error=unknown-command");
}
}
}