123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 |
- // Module for interfacing with file system
- #include "module.h"
- #include "lauxlib.h"
- #include "lmem.h"
- #include "platform.h"
- #include <stdint.h>
- #include "vfs.h"
- #include <string.h>
- #include <alloca.h>
- #define FILE_READ_CHUNK 1024
- // use this time/date in absence of a timestamp
- #define FILE_TIMEDEF_YEAR 1970
- #define FILE_TIMEDEF_MON 01
- #define FILE_TIMEDEF_DAY 01
- #define FILE_TIMEDEF_HOUR 00
- #define FILE_TIMEDEF_MIN 00
- #define FILE_TIMEDEF_SEC 00
- static int file_fd = 0;
- static int file_fd_ref = LUA_NOREF;
- static int rtc_cb_ref = LUA_NOREF;
- typedef struct _file_fd_ud {
- int fd;
- } file_fd_ud;
- static void table2tm( lua_State *L, vfs_time *tm )
- {
- int idx = lua_gettop( L );
- // extract items from table
- lua_getfield( L, idx, "year" );
- lua_getfield( L, idx, "mon" );
- lua_getfield( L, idx, "day" );
- lua_getfield( L, idx, "hour" );
- lua_getfield( L, idx, "min" );
- lua_getfield( L, idx, "sec" );
- tm->year = luaL_optint( L, ++idx, FILE_TIMEDEF_YEAR );
- tm->mon = luaL_optint( L, ++idx, FILE_TIMEDEF_MON );
- tm->day = luaL_optint( L, ++idx, FILE_TIMEDEF_DAY );
- tm->hour = luaL_optint( L, ++idx, FILE_TIMEDEF_HOUR );
- tm->min = luaL_optint( L, ++idx, FILE_TIMEDEF_MIN );
- tm->sec = luaL_optint( L, ++idx, FILE_TIMEDEF_SEC );
- // remove items from stack
- lua_pop( L, 6 );
- }
- static sint32_t file_rtc_cb( vfs_time *tm )
- {
- sint32_t res = VFS_RES_ERR;
- if (rtc_cb_ref != LUA_NOREF) {
- lua_State *L = lua_getstate();
- lua_rawgeti( L, LUA_REGISTRYINDEX, rtc_cb_ref );
- if (luaL_pcallx( L, 0, 1 ) != LUA_OK)
- return res;
- if (lua_type( L, lua_gettop( L ) ) == LUA_TTABLE) {
- table2tm( L, tm );
- res = VFS_RES_OK;
- }
- // pop item returned by callback
- lua_pop( L, 1 );
- }
- return res;
- }
- // Lua: on()
- static int file_on(lua_State *L)
- {
- enum events{
- ON_RTC = 0
- };
- const char *const eventnames[] = {"rtc", NULL};
- int event = luaL_checkoption(L, 1, "rtc", eventnames);
- switch (event) {
- case ON_RTC:
- luaL_unref(L, LUA_REGISTRYINDEX, rtc_cb_ref);
- switch(lua_type(L, 2)) {
- case LUA_TFUNCTION:
- lua_pushvalue(L, 2); // copy argument (func) to the top of stack
- rtc_cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
- vfs_register_rtc_cb(file_rtc_cb);
- break;
- case LUA_TNIL:
- rtc_cb_ref = LUA_NOREF;
- vfs_register_rtc_cb(NULL);
- break;
- default:
- luaL_error(L, "Callback should be function or nil");
- }
- break;
- default:
- break;
- }
- return 0;
- }
- // Lua: close()
- static int file_close( lua_State* L )
- {
- int need_pop = FALSE;
- file_fd_ud *ud;
- if (lua_type( L, 1 ) != LUA_TUSERDATA) {
- // fall back to last opened file
- if (file_fd_ref != LUA_NOREF) {
- lua_rawgeti( L, LUA_REGISTRYINDEX, file_fd_ref );
- // top of stack is now default file descriptor
- ud = (file_fd_ud *)luaL_checkudata(L, -1, "file.obj");
- lua_pop( L, 1 );
- } else {
- // no default file currently opened
- return 0;
- }
- } else {
- ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
- }
- if(ud->fd){
- vfs_close(ud->fd);
- // mark as closed
- ud->fd = 0;
- }
- // unref default file descriptor
- luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref );
- file_fd_ref = LUA_NOREF;
- return 0;
- }
- static int file_obj_free( lua_State *L )
- {
- file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
- if (ud->fd) {
- // close file if it's still open
- vfs_close(ud->fd);
- ud->fd = 0;
- }
- return 0;
- }
- // Lua: format()
- static int file_format( lua_State* L )
- {
- size_t len;
- file_close(L);
- if( !vfs_format() )
- {
- NODE_ERR( "\n*** ERROR ***: unable to format. FS might be compromised.\n" );
- NODE_ERR( "It is advised to re-flash the NodeMCU image.\n" );
- luaL_error(L, "Failed to format file system");
- }
- else{
- NODE_ERR( "format done.\n" );
- }
- return 0;
- }
- static int file_fscfg (lua_State *L)
- {
- uint32_t phys_addr, phys_size;
- vfs_fscfg("/FLASH", &phys_addr, &phys_size);
- lua_pushinteger (L, phys_addr);
- lua_pushinteger (L, phys_size);
- return 2;
- }
- // Lua: open(filename, mode)
- static int file_open( lua_State* L )
- {
- size_t len;
- // unref last file descriptor to allow gc'ing if not kept by user script
- luaL_unref( L, LUA_REGISTRYINDEX, file_fd_ref );
- file_fd_ref = LUA_NOREF;
- const char *fname = luaL_checklstring( L, 1, &len );
- const char *basename = vfs_basename( fname );
- luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid");
- const char *mode = luaL_optstring(L, 2, "r");
- file_fd = vfs_open(fname, mode);
- if(!file_fd){
- lua_pushnil(L);
- } else {
- file_fd_ud *ud = (file_fd_ud *) lua_newuserdata( L, sizeof( file_fd_ud ) );
- ud->fd = file_fd;
- luaL_getmetatable( L, "file.obj" );
- lua_setmetatable( L, -2 );
- // store reference to opened file
- lua_pushvalue( L, -1 );
- file_fd_ref = luaL_ref( L, LUA_REGISTRYINDEX );
- }
- return 1;
- }
- // Lua: list()
- static int file_list( lua_State* L )
- {
- vfs_dir *dir;
- const char *pattern;
- struct vfs_stat stat;
- int pcres;
- lua_settop(L, 1);
- pattern = luaL_optstring(L, 1, NULL); /* Pattern (arg) or nil (not) at 1 */
- dir = vfs_opendir("");
- if (dir == NULL) {
- return 0;
- }
- lua_newtable( L ); /* Table at 2 */
- if (pattern) {
- /*
- * We know that pattern is a string, and so the "match" method will always
- * exist. No need to check return value here
- */
- luaL_getmetafield( L, 1, "match" ); /* Function at 3 */
- }
- while (vfs_readdir(dir, &stat) == VFS_RES_OK) {
- if (pattern) {
- lua_settop( L, 3 ); /* Ensure nothing else on stack */
- /* Construct and pcall(string.match,name,pattern) */
- lua_pushvalue( L, 3 );
- lua_pushstring( L, stat.name );
- lua_pushvalue( L, 1 );
- pcres = lua_pcall( L, 2, 1, 0 );
- if (pcres != 0) {
- vfs_closedir(dir);
- lua_error( L );
- }
- if (lua_isnil( L, -1 )) {
- continue;
- }
- }
- lua_pushinteger( L, stat.size );
- lua_setfield( L, 2, stat.name );
- }
- /* Shed everything back to Table */
- lua_settop( L, 2 );
- vfs_closedir(dir);
- return 1;
- }
- static int get_file_obj( lua_State *L, int *argpos )
- {
- if (lua_type( L, 1 ) == LUA_TUSERDATA) {
- file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
- *argpos = 2;
- return ud->fd;
- } else {
- *argpos = 1;
- return file_fd;
- }
- }
- #define GET_FILE_OBJ int argpos; \
- int fd = get_file_obj( L, &argpos );
- static int file_seek (lua_State *L)
- {
- GET_FILE_OBJ;
- static const int mode[] = {VFS_SEEK_SET, VFS_SEEK_CUR, VFS_SEEK_END};
- static const char *const modenames[] = {"set", "cur", "end", NULL};
- if(!fd)
- return luaL_error(L, "open a file first");
- int op = luaL_checkoption(L, argpos, "cur", modenames);
- long offset = luaL_optlong(L, ++argpos, 0);
- op = vfs_lseek(fd, offset, mode[op]);
- if (op < 0)
- lua_pushnil(L); /* error */
- else
- lua_pushinteger(L, vfs_tell(fd));
- return 1;
- }
- // Lua: exists(filename)
- static int file_exists( lua_State* L )
- {
- size_t len;
- const char *fname = luaL_checklstring( L, 1, &len );
- const char *basename = vfs_basename( fname );
- luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid");
- struct vfs_stat stat;
- lua_pushboolean(L, vfs_stat((char *)fname, &stat) == VFS_RES_OK ? 1 : 0);
- return 1;
- }
- // Lua: remove(filename)
- static int file_remove( lua_State* L )
- {
- size_t len;
- const char *fname = luaL_checklstring( L, 1, &len );
- const char *basename = vfs_basename( fname );
- luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid");
- vfs_remove((char *)fname);
- return 0;
- }
- // Lua: flush()
- static int file_flush( lua_State* L )
- {
- GET_FILE_OBJ;
- if(!fd)
- return luaL_error(L, "open a file first");
- if(vfs_flush(fd) == 0)
- lua_pushboolean(L, 1);
- else
- lua_pushnil(L);
- return 1;
- }
- // Lua: rename("oldname", "newname")
- static int file_rename( lua_State* L )
- {
- size_t len;
- const char *oldname = luaL_checklstring( L, 1, &len );
- const char *basename = vfs_basename( oldname );
- luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(oldname) == len, 1, "filename invalid");
- const char *newname = luaL_checklstring( L, 2, &len );
- basename = vfs_basename( newname );
- luaL_argcheck(L, strlen(basename) <= FS_OBJ_NAME_LEN && strlen(newname) == len, 2, "filename invalid");
- if(0 <= vfs_rename( oldname, newname )){
- lua_pushboolean(L, 1);
- } else {
- lua_pushboolean(L, 0);
- }
- return 1;
- }
- // Lua: stat(filename)
- static int file_stat( lua_State* L )
- {
- size_t len;
- const char *fname = luaL_checklstring( L, 1, &len );
- luaL_argcheck( L, strlen(fname) <= FS_OBJ_NAME_LEN && strlen(fname) == len, 1, "filename invalid" );
- struct vfs_stat stat;
- if (vfs_stat( (char *)fname, &stat ) != VFS_RES_OK) {
- lua_pushnil( L );
- return 1;
- }
- lua_createtable( L, 0, 7 );
- lua_pushinteger( L, stat.size );
- lua_setfield( L, -2, "size" );
- lua_pushstring( L, stat.name );
- lua_setfield( L, -2, "name" );
- lua_pushboolean( L, stat.is_dir );
- lua_setfield( L, -2, "is_dir" );
- lua_pushboolean( L, stat.is_rdonly );
- lua_setfield( L, -2, "is_rdonly" );
- lua_pushboolean( L, stat.is_hidden );
- lua_setfield( L, -2, "is_hidden" );
- lua_pushboolean( L, stat.is_sys );
- lua_setfield( L, -2, "is_sys" );
- lua_pushboolean( L, stat.is_arch );
- lua_setfield( L, -2, "is_arch" );
- // time stamp as sub-table
- lua_createtable( L, 0, 6 );
- lua_pushinteger( L, stat.tm_valid ? stat.tm.year : FILE_TIMEDEF_YEAR );
- lua_setfield( L, -2, "year" );
- lua_pushinteger( L, stat.tm_valid ? stat.tm.mon : FILE_TIMEDEF_MON );
- lua_setfield( L, -2, "mon" );
- lua_pushinteger( L, stat.tm_valid ? stat.tm.day : FILE_TIMEDEF_DAY );
- lua_setfield( L, -2, "day" );
- lua_pushinteger( L, stat.tm_valid ? stat.tm.hour : FILE_TIMEDEF_HOUR );
- lua_setfield( L, -2, "hour" );
- lua_pushinteger( L, stat.tm_valid ? stat.tm.min : FILE_TIMEDEF_MIN );
- lua_setfield( L, -2, "min" );
- lua_pushinteger( L, stat.tm_valid ? stat.tm.sec : FILE_TIMEDEF_SEC );
- lua_setfield( L, -2, "sec" );
- lua_setfield( L, -2, "time" );
- return 1;
- }
- // g_read()
- static int file_g_read( lua_State* L, int n, int16_t end_char, int fd )
- {
- int i, j;
- luaL_Buffer b;
- char p[LUAL_BUFFERSIZE/2];
- if(!fd)
- return luaL_error(L, "open a file first");
- luaL_buffinit(L, &b);
- for (j = 0; j < n; j += sizeof(p)) {
- int nwanted = (n - j >= sizeof(p)) ? sizeof(p) : n - j;
- int nread = vfs_read(fd, p, nwanted);
- if (nread == VFS_RES_ERR || nread == 0) {
- lua_pushnil(L);
- return 1;
- }
- for (i = 0; i < nread; ++i) {
- luaL_addchar(&b, p[i]);
- if (p[i] == end_char) {
- vfs_lseek(fd, -nread + j + i + 1, VFS_SEEK_CUR); //reposition after end char found
- nread = 0; // force break on outer loop
- break;
- }
- }
- if (nread < nwanted)
- break;
- }
- luaL_pushresult(&b);
- return 1;
- }
- // Lua: read()
- // file.read() will read FILE _CHUNK bytes, or EOF is reached.
- // file.read(10) will read 10 byte from file, or EOF is reached.
- // file.read('q') will read until 'q' or EOF is reached.
- static int file_read( lua_State* L )
- {
- unsigned need_len = FILE_READ_CHUNK;
- int16_t end_char = EOF;
- size_t el;
- GET_FILE_OBJ;
- if( lua_type( L, argpos ) == LUA_TNUMBER )
- {
- need_len = ( unsigned )luaL_checkinteger( L, argpos );
- }
- else if(lua_isstring(L, argpos))
- {
- const char *end = luaL_checklstring( L, argpos, &el );
- if(el!=1){
- return luaL_error( L, "wrong arg range" );
- }
- end_char = (int16_t)end[0];
- }
- return file_g_read(L, need_len, end_char, fd);
- }
- // Lua: readline()
- static int file_readline( lua_State* L )
- {
- GET_FILE_OBJ;
- return file_g_read(L, FILE_READ_CHUNK, '\n', fd);
- }
- // Lua: getfile(filename)
- static int file_getfile( lua_State* L )
- {
- // Warning this code C calls other file_* routines to avoid duplication code. These
- // use Lua stack addressing of arguments, so this does Lua stack maniplation to
- // align these
- int ret_cnt = 0;
- lua_settop(L ,1);
- // Stack [1] = FD
- file_open(L);
- // Stack [1] = filename; [2] = FD or nil
- if (!lua_isnil(L, -1)) {
- lua_remove(L, 1); // dump filename, so [1] = FD
- file_fd_ud *ud = (file_fd_ud *)luaL_checkudata(L, 1, "file.obj");
- ret_cnt = file_g_read(L, LUAI_MAXINT32, EOF, ud->fd);
- // Stack [1] = FD; [2] = contents if ret_cnt = 1;
- file_close(L); // leaves Stack unchanged if [1] = FD
- lua_remove(L, 1); // Dump FD leaving contents as [1] / ToS
- }
- return ret_cnt;
- }
- // Lua: write("string")
- static int file_write( lua_State* L )
- {
- GET_FILE_OBJ;
- if(!fd)
- return luaL_error(L, "open a file first");
- size_t l, rl;
- const char *s = luaL_checklstring(L, argpos, &l);
- rl = vfs_write(fd, s, l);
- if(rl==l)
- lua_pushboolean(L, 1);
- else
- lua_pushnil(L);
- return 1;
- }
- // Lua: writeline("string")
- static int file_writeline( lua_State* L )
- {
- GET_FILE_OBJ;
- if(!fd)
- return luaL_error(L, "open a file first");
- size_t l, rl;
- const char *s = luaL_checklstring(L, argpos, &l);
- rl = vfs_write(fd, s, l);
- if(rl==l){
- rl = vfs_write(fd, "\n", 1);
- if(rl==1)
- lua_pushboolean(L, 1);
- else
- lua_pushnil(L);
- }
- else{
- lua_pushnil(L);
- }
- return 1;
- }
- // Lua: getfile(filename)
- static int file_putfile( lua_State* L )
- {
- // Warning this code C calls other file_* routines to avoid duplication code. These
- // use Lua stack addressing of arguments, so this does Lua stack maniplation to
- // align these
- int ret_cnt = 0;
- lua_settop(L, 2);
- lua_pushvalue(L, 2); //dup contents onto the ToS [3]
- lua_pushliteral(L, "w+");
- lua_replace(L, 2);
- // Stack [1] = filename; [2] "w+" [3] contents;
- file_open(L);
- // Stack [1] = filename; [2] "w+" [3] contents; [4] FD or nil
- if (!lua_isnil(L, -1)) {
- lua_remove(L, 2); //dump "w+" attribute literal
- lua_replace(L, 1);
- // Stack [1] = FD; [2] contents
- file_write(L);
- // Stack [1] = FD; [2] contents; [3] result status
- lua_remove(L, 2); //dump contents
- file_close(L);
- lua_remove(L, 1); // Dump FD leaving status as ToS
- }
- return 1;
- }
- // Lua: fsinfo()
- static int file_fsinfo( lua_State* L )
- {
- uint32_t total, used;
- if (vfs_fsinfo("", &total, &used)) {
- return luaL_error(L, "file system failed");
- }
- NODE_DBG("total: %d, used:%d\n", total, used);
- if(total>0x7FFFFFFF || used>0x7FFFFFFF || used > total)
- {
- return luaL_error(L, "file system error");
- }
- lua_pushinteger(L, total-used);
- lua_pushinteger(L, used);
- lua_pushinteger(L, total);
- return 3;
- }
- typedef struct {
- vfs_vol *vol;
- } volume_type;
- // Lua: vol = file.mount("/SD0")
- static int file_mount( lua_State *L )
- {
- const char *ldrv = luaL_checkstring( L, 1 );
- int num = luaL_optint( L, 2, -1 );
- volume_type *vol = (volume_type *)lua_newuserdata( L, sizeof( volume_type ) );
- if (vol->vol = vfs_mount( ldrv, num )) {
- /* set its metatable */
- luaL_getmetatable(L, "file.vol");
- lua_setmetatable(L, -2);
- return 1;
- } else {
- // remove created userdata
- lua_pop( L, 1 );
- return 0;
- }
- }
- // Lua: success = file.chdir("/SD0/")
- static int file_chdir( lua_State *L )
- {
- const char *path = luaL_checkstring( L, 1 );
- lua_pushboolean( L, 0 <= vfs_chdir( path ) );
- return 1;
- }
- static int file_vol_umount( lua_State *L )
- {
- volume_type *vol = luaL_checkudata( L, 1, "file.vol" );
- luaL_argcheck( L, vol, 1, "volume expected" );
- lua_pushboolean( L, 0 <= vfs_umount( vol->vol ) );
- // invalidate vfs descriptor, it has been free'd anyway
- vol->vol = NULL;
- return 1;
- }
- LROT_BEGIN(file_obj, NULL, LROT_MASK_GC_INDEX)
- LROT_FUNCENTRY( __gc, file_obj_free )
- LROT_TABENTRY( __index, file_obj )
- LROT_FUNCENTRY( close, file_close )
- LROT_FUNCENTRY( read, file_read )
- LROT_FUNCENTRY( readline, file_readline )
- LROT_FUNCENTRY( write, file_write )
- LROT_FUNCENTRY( writeline, file_writeline )
- LROT_FUNCENTRY( seek, file_seek )
- LROT_FUNCENTRY( flush, file_flush )
- LROT_END(file_obj, NULL, LROT_MASK_GC_INDEX)
- LROT_BEGIN(file_vol, NULL, LROT_MASK_INDEX)
- LROT_TABENTRY( __index, file_vol )
- LROT_FUNCENTRY( umount, file_vol_umount )
- LROT_END(file_vol, NULL, LROT_MASK_INDEX)
- // Module function map
- LROT_BEGIN(file, NULL, 0)
- LROT_FUNCENTRY( list, file_list )
- LROT_FUNCENTRY( open, file_open )
- LROT_FUNCENTRY( close, file_close )
- LROT_FUNCENTRY( write, file_write )
- LROT_FUNCENTRY( writeline, file_writeline )
- LROT_FUNCENTRY( read, file_read )
- LROT_FUNCENTRY( readline, file_readline )
- #ifdef BUILD_SPIFFS
- LROT_FUNCENTRY( format, file_format )
- LROT_FUNCENTRY( fscfg, file_fscfg )
- #endif
- LROT_FUNCENTRY( remove, file_remove )
- LROT_FUNCENTRY( seek, file_seek )
- LROT_FUNCENTRY( flush, file_flush )
- LROT_FUNCENTRY( rename, file_rename )
- LROT_FUNCENTRY( exists, file_exists )
- LROT_FUNCENTRY( getcontents, file_getfile )
- LROT_FUNCENTRY( putcontents, file_putfile )
- LROT_FUNCENTRY( fsinfo, file_fsinfo )
- LROT_FUNCENTRY( on, file_on )
- LROT_FUNCENTRY( stat, file_stat )
- #ifdef BUILD_FATFS
- LROT_FUNCENTRY( mount, file_mount )
- LROT_FUNCENTRY( chdir, file_chdir )
- #endif
- LROT_END(file, NULL, 0)
- int luaopen_file( lua_State *L ) {
- if (!vfs_mount("/FLASH", 0)) {
- // Failed to mount -- try reformat
- dbg_printf("Formatting file system. Please wait...\n");
- if (!vfs_format()) {
- NODE_ERR( "\n*** ERROR ***: unable to format. FS might be compromised.\n" );
- NODE_ERR( "It is advised to re-flash the NodeMCU image.\n" );
- }
- }
- luaL_rometatable( L, "file.vol", LROT_TABLEREF(file_vol));
- luaL_rometatable( L, "file.obj", LROT_TABLEREF(file_obj));
- return 0;
- }
- NODEMCU_MODULE(FILE, "file", file, luaopen_file);
|