#include "module.h" #include "lauxlib.h" #include "esp_vfs.h" #include #include #include #include /** * The logical layout of the embedded volumes is * [ volume record ] * [ volume record ] * ... * [ index record in vol1 ] * [ index record in vol1 ] * ... * [ file contents in vol1 ] * [ file contents in vol1 ] * ... * [ index record in vol2 ] * [ index record in vol2 ] * ... * [ file contents in vol2 ] * [ file contents in vol2 ] * * Both the volume records and index records are variable length so as to not * waste space in their name fields. Finding the start of the index records * for a volume is by reading the offs(et) field in the volume record and * jumping that many bytes forward from the start of the eromfs.bin data. * Similarly, finding the file contents is by reading the index record's * offs(et) field and basing that off the start of the volume index. * Naturally, the start of the volume index is the same as the end of the * volume header, and the start of the file contents is the same as the * end of the volume index, and either of those can be worked out by * reading the offs(et) field in the first record. */ #pragma pack(push, 1) typedef struct { uint8_t rec_len; uint16_t offs; // index_offs char name[]; } volume_record_t; typedef struct { uint8_t rec_len; uint32_t offs; // based off index_offs uint32_t len; // file_len char name[]; } index_record_t; #pragma pack(pop) typedef struct { const index_record_t *meta; const char *data; // start of data off_t pos; } file_descriptor_t; typedef struct { DIR opaque; const index_record_t *index; const index_record_t *pos; } eromfs_DIR_t; extern const char eromfs_bin_start[] asm("_binary_eromfs_bin_start"); // Both the volume header and the file set indices end where the next // type of data block commences (file set index, file contents). #define end_of(type, start) ((const type *)(((char *)start) + start->offs)) #define eromfs_header_start ((const volume_record_t *)eromfs_bin_start) #define eromfs_header_end end_of(volume_record_t, eromfs_header_start) /* The logic for finding a volume record by name is the same as finding a * file record by name, only the data structure type varies. Hence we * hide the casting and variable length record stepping behind a convenience * macro here. */ #define find_entry_by_name(out, xname, start_void_p, record_t) \ do { \ const record_t *entry_ = (const record_t *)(start_void_p); \ const record_t *end_ = end_of(record_t, entry_); \ unsigned xname_len = strlen(xname); \ for (; entry_ < end_; \ entry_ = (const record_t *)(((char *)entry_) + entry_->rec_len)) \ { \ uint8_t name_len = entry_->rec_len - sizeof(record_t); \ if (xname_len == name_len && \ strncmp(xname, entry_->name, name_len) == 0) \ { \ out = entry_; \ break; \ } \ } \ } while(0); static int mounted_volumes = LUA_NOREF; static SemaphoreHandle_t fd_mutex; static file_descriptor_t fds[CONFIG_NODEMCU_MAX_OPEN_FILES]; // --- VFS interface ----------------------------------------------------- #define get_index() const index_record_t *index = (const index_record_t *)ctx static const index_record_t *path2entry(void *ctx, const char *path) { while (*path == '/') ++path; get_index(); const index_record_t *entry = NULL; find_entry_by_name(entry, path, index, index_record_t); return entry; } static int eromfs_fstat(void *ctx, int fd, struct stat *st) { memset(st, 0, sizeof(struct stat)); st->st_size = fds[fd].meta->len; st->st_blocks = (fds[fd].meta->len + 511)/512; return 0; } #ifdef CONFIG_VFS_SUPPORT_DIR static int eromfs_stat(void *ctx, const char *path, struct stat *st) { const index_record_t *entry = path2entry(ctx, path); if (!entry) return -ENOENT; memset(st, 0, sizeof(struct stat)); st->st_size = entry->len; st->st_blocks = (entry->len + 511)/512; return 0; } static DIR *eromfs_opendir(void *ctx, const char *path) { if (strcmp(path, "/") != 0) return NULL; get_index(); eromfs_DIR_t *dir = calloc(1, sizeof(eromfs_DIR_t)); dir->index = index; dir->pos = index; return (DIR *)dir; } static struct dirent *eromfs_readdir(void *ctx, DIR *pdir) { UNUSED(ctx); eromfs_DIR_t *dir = (eromfs_DIR_t *)pdir; const index_record_t *end = end_of(index_record_t, dir->index); if (dir->pos >= end) return NULL; static struct dirent de = { .d_ino = 0, .d_type = DT_REG, }; size_t max_len = sizeof(de.d_name); size_t len = dir->pos->rec_len - sizeof(index_record_t); if (len > max_len -1) len = max_len - 1; strncpy(de.d_name, dir->pos->name, len); de.d_name[len] = 0; dir->pos = (const index_record_t *)((char *)dir->pos + dir->pos->rec_len); return &de; } static int eromfs_closedir(void *ctx, DIR *dir) { UNUSED(ctx); free(dir); return 0; } #endif static int eromfs_open(void *ctx, const char *path, int flags, int mode) { UNUSED(flags); UNUSED(mode); const index_record_t *entry = path2entry(ctx, path); if (!entry) return -ENOENT; xSemaphoreTake(fd_mutex, portMAX_DELAY); int fd = -ENFILE; // max open files is guaranteed to be small; linear search is fine for (unsigned i = 0; i < CONFIG_NODEMCU_MAX_OPEN_FILES; ++i) { if (fds[i].meta == NULL) { fds[i].meta = entry; fds[i].data = (const char *)ctx + entry->offs; fds[i].pos = 0; fd = (int)i; } } xSemaphoreGive(fd_mutex); return fd; } static ssize_t eromfs_read(void *ctx, int fd, void *dst, size_t size) { UNUSED(ctx); size_t avail = fds[fd].meta->len - fds[fd].pos; if (size > avail) size = avail; const char *src = fds[fd].data + fds[fd].pos; memcpy(dst, src, size); fds[fd].pos += size; return size; } static off_t eromfs_lseek(void *ctx, int fd, off_t size, int mode) { UNUSED(ctx); off_t pos = fds[fd].pos; switch(mode) { case SEEK_SET: pos = size; break; case SEEK_CUR: pos += size; break; case SEEK_END: pos = fds[fd].meta->len + size; break; default: return -EINVAL; } if (pos < 0 || pos > fds[fd].meta->len) return -EINVAL; fds[fd].pos = pos; return pos; } static int eromfs_close(void *ctx, int fd) { UNUSED(ctx); xSemaphoreTake(fd_mutex, portMAX_DELAY); fds[fd].meta = NULL; fds[fd].data = NULL; fds[fd].pos = 0; xSemaphoreGive(fd_mutex); return 0; } // --- Lua interface ----------------------------------------------------- static int leromfs_list(lua_State *L) { lua_newtable(L); int t = lua_gettop(L); // If this logic looks similar to the find_entry_by_name() macro, it's // because it is :) Except we're capturing all the volume names, so no // easy reuse. const volume_record_t *vol = eromfs_header_start; const volume_record_t *end = eromfs_header_end; for (; vol < end; vol = (const volume_record_t *)((char *)vol + vol->rec_len)) { uint8_t volume_name_len = vol->rec_len - sizeof(volume_record_t); lua_pushlstring(L, vol->name, volume_name_len); lua_rawseti(L, t, lua_objlen(L, t) + 1); } return 1; } static int leromfs_mount(lua_State *L) { const char *name = luaL_checkstring(L, 1); const char *mountpt = luaL_checkstring(L, 2); lua_settop(L, 2); const volume_record_t *vol = NULL; find_entry_by_name(vol, name, eromfs_bin_start, volume_record_t); if (!vol) return luaL_error(L, "volume %s not found", name); const index_record_t *index_start = (const index_record_t *)(eromfs_bin_start + vol->offs); esp_vfs_t eromfs = { .flags = ESP_VFS_FLAG_CONTEXT_PTR, .open_p = eromfs_open, .fstat_p = eromfs_fstat, .read_p = eromfs_read, .lseek_p = eromfs_lseek, .close_p = eromfs_close, #ifdef CONFIG_VFS_SUPPORT_DIR .stat_p = eromfs_stat, .opendir_p = eromfs_opendir, .readdir_p = eromfs_readdir, .closedir_p = eromfs_closedir, #endif }; esp_err_t err = esp_vfs_register(mountpt, &eromfs, (void *)index_start); if (err != ESP_OK) return luaL_error(L, "failed to mount eromfs; code %d", err); lua_rawgeti(L, LUA_REGISTRYINDEX, mounted_volumes); lua_pushvalue(L, 2); lua_pushvalue(L, 1); lua_rawset(L, -3); // mounted_volumes[mountpt] = name return 0; } static int leromfs_unmount(lua_State *L) { const char *name = luaL_checkstring(L, 1); const char *mountpt = luaL_checkstring(L, 2); lua_settop(L, 2); lua_rawgeti(L, LUA_REGISTRYINDEX, mounted_volumes); lua_pushvalue(L, 2); lua_rawget(L, -2); if (lua_isstring(L, -1)) { const char *mounted_name = lua_tostring(L, -1); if (strcmp(name, mounted_name) == 0) { esp_err_t err = esp_vfs_unregister(mountpt); if (err != ESP_OK) return luaL_error(L, "unmounting failed; code %d", err); lua_pop(L, 1); lua_pushvalue(L, 2); lua_pushnil(L); lua_rawset(L, -3); // mounted_volumes[mountpt] = nil return 0; } else return luaL_error(L, "can't umount %s from %s; volume %s is mounted there", name, mountpt, mounted_name); } else return 0; // already unmounted, not an error } static int leromfs_init(lua_State *L) { fd_mutex = xSemaphoreCreateMutex(); lua_newtable(L); mounted_volumes = luaL_ref(L, LUA_REGISTRYINDEX); return 0; } LROT_BEGIN(eromfs, NULL, 0) LROT_FUNCENTRY( list, leromfs_list ) LROT_FUNCENTRY( mount, leromfs_mount ) LROT_FUNCENTRY( unmount, leromfs_unmount ) LROT_END(eromfs, NULL, 0) NODEMCU_MODULE(EROMFS, "eromfs", eromfs, leromfs_init);