380 lines
9.4 KiB
C
380 lines
9.4 KiB
C
|
#include "module.h"
|
||
|
#include "lauxlib.h"
|
||
|
|
||
|
#include "esp_vfs.h"
|
||
|
#include <errno.h>
|
||
|
#include <dirent.h>
|
||
|
|
||
|
#include <stdint.h>
|
||
|
#include <string.h>
|
||
|
|
||
|
/**
|
||
|
* 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);
|