570 lines
18 KiB
C
570 lines
18 KiB
C
/*
|
|
** $Id: lflash.c
|
|
** See Copyright Notice in lua.h
|
|
*/
|
|
|
|
#define lflash_c
|
|
#define LUA_CORE
|
|
#define LUAC_CROSS_FILE
|
|
#include "lua.h"
|
|
|
|
#include "lobject.h"
|
|
#include "lauxlib.h"
|
|
#include "lstate.h"
|
|
#include "lfunc.h"
|
|
#include "lflash.h"
|
|
#include "platform.h"
|
|
#include "user_interface.h"
|
|
#include "vfs.h"
|
|
#include "uzlib.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/*
|
|
* Flash memory is a fixed memory addressable block that is serially allocated by the
|
|
* luac build process and the out image can be downloaded into SPIFSS and loaded into
|
|
* flash with a node.flash.load() command. See luac_cross/lflashimg.c for the build
|
|
* process.
|
|
*/
|
|
|
|
static char *flashAddr;
|
|
static uint32_t flashSize;
|
|
static uint32_t flashAddrPhys;
|
|
static uint32_t flashSector;
|
|
static uint32_t curOffset;
|
|
|
|
#define ALIGN(s) (((s)+sizeof(size_t)-1) & ((size_t) (- (signed) sizeof(size_t))))
|
|
#define ALIGN_BITS(s) (((uint32_t)s) & (sizeof(size_t)-1))
|
|
#define ALL_SET (~0)
|
|
#define FLASH_PAGE_SIZE INTERNAL_FLASH_SECTOR_SIZE
|
|
#define FLASH_PAGES (flashSize/FLASH_PAGE_SIZE)
|
|
#define READ_BLOCKSIZE 1024
|
|
#define WRITE_BLOCKSIZE 2048
|
|
#define DICTIONARY_WINDOW 16384
|
|
#define WORDSIZE (sizeof(int))
|
|
#define BITS_PER_WORD 32
|
|
#define WRITE_BLOCKS ((DICTIONARY_WINDOW/WRITE_BLOCKSIZE)+1)
|
|
#define WRITE_BLOCK_WORDS (WRITE_BLOCKSIZE/WORDSIZE)
|
|
|
|
struct INPUT {
|
|
int fd;
|
|
int len;
|
|
uint8_t block[READ_BLOCKSIZE];
|
|
uint8_t *inPtr;
|
|
int bytesRead;
|
|
int left;
|
|
void *inflate_state;
|
|
} *in;
|
|
|
|
typedef struct {
|
|
uint8_t byte[WRITE_BLOCKSIZE];
|
|
} outBlock;
|
|
|
|
struct OUTPUT {
|
|
lua_State *L;
|
|
lu_int32 flash_sig;
|
|
int len;
|
|
outBlock *block[WRITE_BLOCKS];
|
|
outBlock buffer;
|
|
int ndx;
|
|
uint32_t crc;
|
|
void (*fullBlkCB) (void);
|
|
int flashLen;
|
|
int flagsLen;
|
|
int flagsNdx;
|
|
uint32_t *flags;
|
|
const char *error;
|
|
} *out;
|
|
|
|
#ifdef NODE_DEBUG
|
|
void dumpStrt(stringtable *tb, const char *type) {
|
|
int i,j;
|
|
GCObject *o;
|
|
|
|
NODE_DBG("\nDumping %s String table\n\n========================\n", type);
|
|
NODE_DBG("No of elements: %d\nSize of table: %d\n", tb->nuse, tb->size);
|
|
for (i=0; i<tb->size; i++)
|
|
for(o = tb->hash[i], j=0; o; (o=o->gch.next), j++ ) {
|
|
TString *ts =cast(TString *, o);
|
|
NODE_DBG("%5d %5d %08x %08x %5d %1s %s\n",
|
|
i, j, (size_t) ts, ts->tsv.hash, ts->tsv.len,
|
|
ts_isreadonly(ts) ? "R" : " ", getstr(ts));
|
|
}
|
|
}
|
|
|
|
LUA_API void dumpStrings(lua_State *L) {
|
|
dumpStrt(&G(L)->strt, "RAM");
|
|
if (G(L)->ROstrt.hash)
|
|
dumpStrt(&G(L)->ROstrt, "ROM");
|
|
}
|
|
#endif
|
|
|
|
/* =====================================================================================
|
|
* The next 4 functions: flashPosition, flashSetPosition, flashBlock and flashErase
|
|
* wrap writing to flash. The last two are platform dependent. Also note that any
|
|
* writes are suppressed if the global writeToFlash is false. This is used in
|
|
* phase I where the pass is used to size the structures in flash.
|
|
*/
|
|
static char *flashPosition(void){
|
|
return flashAddr + curOffset;
|
|
}
|
|
|
|
|
|
static char *flashSetPosition(uint32_t offset){
|
|
NODE_DBG("flashSetPosition(%04x)\n", offset);
|
|
curOffset = offset;
|
|
return flashPosition();
|
|
}
|
|
|
|
|
|
static char *flashBlock(const void* b, size_t size) {
|
|
void *cur = flashPosition();
|
|
NODE_DBG("flashBlock((%04x),%p,%04x)\n", curOffset,b,size);
|
|
lua_assert(ALIGN_BITS(b) == 0 && ALIGN_BITS(size) == 0);
|
|
platform_flash_write(b, flashAddrPhys+curOffset, size);
|
|
curOffset += size;
|
|
return cur;
|
|
}
|
|
|
|
|
|
static void flashErase(uint32_t start, uint32_t end){
|
|
int i;
|
|
if (start == -1) start = FLASH_PAGES - 1;
|
|
if (end == -1) end = FLASH_PAGES - 1;
|
|
NODE_DBG("flashErase(%04x,%04x)\n", flashSector+start, flashSector+end);
|
|
for (i = start; i<=end; i++)
|
|
platform_flash_erase_sector( flashSector + i );
|
|
}
|
|
|
|
/* =====================================================================================
|
|
* luaN_init(), luaN_reload_reboot() and luaN_index() are exported via lflash.h.
|
|
* The first is the startup hook used in lstate.c and the last two are
|
|
* implementations of the node.flash API calls.
|
|
*/
|
|
|
|
/*
|
|
* Hook in lstate.c:f_luaopen() to set up ROstrt and ROpvmain if needed
|
|
*/
|
|
LUAI_FUNC void luaN_init (lua_State *L) {
|
|
|
|
flashSize = platform_flash_get_partition (NODEMCU_LFS0_PARTITION, &flashAddrPhys);
|
|
if (flashSize == 0) {
|
|
return; // Nothing to do if the size is zero
|
|
}
|
|
G(L)->LFSsize = flashSize;
|
|
flashAddr = cast(char *, platform_flash_phys2mapped(flashAddrPhys));
|
|
flashSector = platform_flash_get_sector_of_address(flashAddrPhys);
|
|
FlashHeader *fh = cast(FlashHeader *, flashAddr);
|
|
curOffset = 0;
|
|
|
|
/*
|
|
* For the LFS to be valid, its signature has to be correct for this build
|
|
* variant, the ROhash and main proto fields must be defined and the main proto
|
|
* address be within the LFS address bounds. (This last check is primarily to
|
|
* detect the direct imaging of an absolute LFS with the wrong base address.
|
|
*/
|
|
|
|
if (fh->flash_sig == 0 || fh->flash_sig == ~0 ) {
|
|
NODE_ERR("No LFS image loaded\n");
|
|
return;
|
|
}
|
|
|
|
if ((fh->flash_sig & (~FLASH_SIG_ABSOLUTE)) != FLASH_SIG ) {
|
|
NODE_ERR("Flash sig not correct: 0x%08x vs 0x%08x\n",
|
|
fh->flash_sig & (~FLASH_SIG_ABSOLUTE), FLASH_SIG);
|
|
return;
|
|
}
|
|
|
|
if (fh->pROhash == ALL_SET ||
|
|
((fh->mainProto - cast(FlashAddr, fh)) >= fh->flash_size)) {
|
|
NODE_ERR("Flash size check failed: 0x%08x vs 0xFFFFFFFF; 0x%08x >= 0x%08x\n",
|
|
fh->pROhash, fh->mainProto - cast(FlashAddr, fh), fh->flash_size);
|
|
return;
|
|
}
|
|
|
|
G(L)->ROstrt.hash = cast(GCObject **, fh->pROhash);
|
|
G(L)->ROstrt.nuse = fh->nROuse ;
|
|
G(L)->ROstrt.size = fh->nROsize;
|
|
G(L)->ROpvmain = cast(Proto *,fh->mainProto);
|
|
}
|
|
|
|
//extern void software_reset(void);
|
|
static int loadLFS (lua_State *L);
|
|
static int loadLFSgc (lua_State *L);
|
|
static void procFirstPass (void);
|
|
|
|
/*
|
|
* Library function called by node.flashreload(filename).
|
|
*/
|
|
LUALIB_API int luaN_reload_reboot (lua_State *L) {
|
|
// luaL_dbgbreak();
|
|
const char *fn = lua_tostring(L, 1), *msg = "";
|
|
int status;
|
|
|
|
if (G(L)->LFSsize == 0) {
|
|
lua_pushstring(L, "No LFS partition allocated");
|
|
return 1;
|
|
}
|
|
|
|
|
|
/*
|
|
* Do a protected call of loadLFS.
|
|
*
|
|
* - This will normally rewrite the LFS and reboot, with no return.
|
|
* - If an error occurs then it is sent to the UART.
|
|
* - If this occured in the 1st pass, the previous LFS is unchanged so it is
|
|
* safe to return to the calling Lua.
|
|
* - If in the 1st pass, then the ESP is rebooted.
|
|
*/
|
|
status = lua_cpcall(L, &loadLFS, cast(void *,fn));
|
|
|
|
if (!out || out->fullBlkCB == procFirstPass) {
|
|
/*
|
|
* Never entered the 2nd pass, so it is safe to return the error. Note
|
|
* that I've gone to some trouble to ensure that all dynamically allocated
|
|
* working areas have been freed, so that we have no memory leaks.
|
|
*/
|
|
if (status == LUA_ERRMEM)
|
|
msg = "Memory allocation error";
|
|
else if (out && out->error)
|
|
msg = out->error;
|
|
else
|
|
msg = "Unknown Error";
|
|
|
|
/* We can clean up and return error */
|
|
lua_cpcall(L, &loadLFSgc, NULL);
|
|
lua_settop(L, 0);
|
|
lua_pushstring(L, msg);
|
|
return 1;
|
|
}
|
|
|
|
if (status == 0) {
|
|
/* Successful LFS rewrite */
|
|
msg = "LFS region updated. Restarting.";
|
|
} else {
|
|
/* We have errored during the second pass so clear the LFS and reboot */
|
|
if (status == LUA_ERRMEM)
|
|
msg = "Memory allocation error";
|
|
else if (out->error)
|
|
msg = out->error;
|
|
else
|
|
msg = "Unknown Error";
|
|
|
|
flashErase(0,-1);
|
|
}
|
|
NODE_ERR(msg);
|
|
|
|
while (1) {} // Force WDT as the ROM software_reset() doesn't seem to work
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
* If the arg is a valid LFS module name then return the LClosure
|
|
* pointing to it. Otherwise return:
|
|
* - The Unix time that the LFS was built
|
|
* - The base address and length of the LFS
|
|
* - An array of the module names in the LFS
|
|
*/
|
|
LUAI_FUNC int luaN_index (lua_State *L) {
|
|
int n = lua_gettop(L);
|
|
|
|
/* Return nil + the LFS base address if the LFS size > 0 and it isn't loaded */
|
|
if (!(G(L)->ROpvmain)) {
|
|
lua_settop(L, 0);
|
|
lua_pushnil(L);
|
|
if (G(L)->LFSsize) {
|
|
lua_pushinteger(L, (lua_Integer) flashAddr);
|
|
lua_pushinteger(L, flashAddrPhys);
|
|
lua_pushinteger(L, G(L)->LFSsize);
|
|
return 4;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Push the LClosure of the LFS index function */
|
|
Closure *cl = luaF_newLclosure(L, 0, hvalue(gt(L)));
|
|
cl->l.p = G(L)->ROpvmain;
|
|
lua_settop(L, n+1);
|
|
setclvalue(L, L->top-1, cl);
|
|
|
|
/* Move it infront of the arguments and call the index function */
|
|
lua_insert(L, 1);
|
|
lua_call(L, n, LUA_MULTRET);
|
|
|
|
/* Return it if the response if a single value (the function) */
|
|
if (lua_gettop(L) == 1)
|
|
return 1;
|
|
|
|
lua_assert(lua_gettop(L) == 2);
|
|
|
|
/* Otherwise add the base address of the LFS, and its size bewteen the */
|
|
/* Unix time and the module list, then return all 4 params. */
|
|
lua_pushinteger(L, (lua_Integer) flashAddr);
|
|
lua_insert(L, 2);
|
|
lua_pushinteger(L, flashAddrPhys);
|
|
lua_insert(L, 3);
|
|
lua_pushinteger(L, cast(FlashHeader *, flashAddr)->flash_size);
|
|
lua_insert(L, 4);
|
|
return 5;
|
|
}
|
|
/* =====================================================================================
|
|
* The following routines use my uzlib which was based on pfalcon's inflate and
|
|
* deflate routines. The standard NodeMCU make also makes two host tools uz_zip
|
|
* and uz_unzip which also use these and luac.cross uses the deflate. As discussed
|
|
* below, The main action routine loadLFS() calls uzlib_inflate() to do the actual
|
|
* stream inflation but uses three supplied CBs to abstract input and output
|
|
* stream handling.
|
|
*
|
|
* ESP8266 RAM limitations and heap fragmentation are a key implementation
|
|
* constraint and hence these routines use a number of ~2K buffers (11) as
|
|
* working storage.
|
|
*
|
|
* The inflate is done twice, in order to limit storage use and avoid forward /
|
|
* backward reference issues. However this has a major advantage that the LFS
|
|
* is scanned with the headers, CRC, etc. validated BEFORE the write to flash
|
|
* is started, so the only real chance of failure during the second pass
|
|
* write is if a power fail occurs during the pass.
|
|
*/
|
|
|
|
static void flash_error(const char *err) {
|
|
if (out)
|
|
out->error = err;
|
|
if (in && in->inflate_state)
|
|
uz_free(in->inflate_state);
|
|
lua_pushnil(out->L); /* can't use it on a cpcall anyway */
|
|
lua_error(out->L);
|
|
}
|
|
|
|
/*
|
|
* uzlib_inflate does a stream inflate on an RFC 1951 encoded data stream.
|
|
* It uses three application-specific CBs passed in the call to do the work:
|
|
*
|
|
* - get_byte() CB to return next byte in input stream
|
|
* - put_byte() CB to output byte to output buffer
|
|
* - recall_byte() CB to output byte to retrieve a historic byte from
|
|
* the output buffer.
|
|
*
|
|
* Note that put_byte() also triggers secondary CBs to do further processing.
|
|
*/
|
|
static uint8_t get_byte (void) {
|
|
if (--in->left < 0) {
|
|
/* Read next input block */
|
|
int remaining = in->len - in->bytesRead;
|
|
int wanted = remaining >= READ_BLOCKSIZE ? READ_BLOCKSIZE : remaining;
|
|
|
|
if (vfs_read(in->fd, in->block, wanted) != wanted)
|
|
flash_error("read error on LFS image file");
|
|
|
|
system_soft_wdt_feed();
|
|
|
|
in->bytesRead += wanted;
|
|
in->inPtr = in->block;
|
|
in->left = wanted-1;
|
|
}
|
|
return *in->inPtr++;
|
|
}
|
|
|
|
|
|
static void put_byte (uint8_t value) {
|
|
int offset = out->ndx % WRITE_BLOCKSIZE; /* counts from 0 */
|
|
|
|
out->block[0]->byte[offset++] = value;
|
|
out->ndx++;
|
|
|
|
if (offset == WRITE_BLOCKSIZE || out->ndx == out->len) {
|
|
if (out->fullBlkCB)
|
|
out->fullBlkCB();
|
|
/* circular shift the block pointers (redundant on last block, but so what) */
|
|
outBlock *nextBlock = out->block[WRITE_BLOCKS - 1];
|
|
memmove(out->block+1, out->block, (WRITE_BLOCKS-1)*sizeof(void*));
|
|
out->block[0] = nextBlock ;
|
|
}
|
|
}
|
|
|
|
|
|
static uint8_t recall_byte (unsigned offset) {
|
|
if(offset > DICTIONARY_WINDOW || offset >= out->ndx)
|
|
flash_error("invalid dictionary offset on inflate");
|
|
/* ndx starts at 1. Need relative to 0 */
|
|
unsigned n = out->ndx - offset;
|
|
unsigned pos = n % WRITE_BLOCKSIZE;
|
|
unsigned blockNo = out->ndx / WRITE_BLOCKSIZE - n / WRITE_BLOCKSIZE;
|
|
return out->block[blockNo]->byte[pos];
|
|
}
|
|
|
|
/*
|
|
* On the first pass the break index is set to call this process at the end
|
|
* of each completed output buffer.
|
|
* - On the first call, the Flash Header is checked.
|
|
* - On each call the CRC is rolled up for that buffer.
|
|
* - Once the flags array is in-buffer this is also captured.
|
|
* This logic is slightly complicated by the last buffer is typically short.
|
|
*/
|
|
void procFirstPass (void) {
|
|
int len = (out->ndx % WRITE_BLOCKSIZE) ?
|
|
out->ndx % WRITE_BLOCKSIZE : WRITE_BLOCKSIZE;
|
|
if (out->ndx <= WRITE_BLOCKSIZE) {
|
|
/* Process the flash header and cache the FlashHeader fields we need */
|
|
FlashHeader *fh = cast(FlashHeader *, out->block[0]);
|
|
out->flashLen = fh->flash_size; /* in bytes */
|
|
out->flagsLen = (out->len-fh->flash_size)/WORDSIZE; /* in words */
|
|
out->flash_sig = fh->flash_sig;
|
|
|
|
if ((fh->flash_sig & FLASH_FORMAT_MASK) != FLASH_FORMAT_VERSION)
|
|
flash_error("Incorrect LFS header version");
|
|
if ((fh->flash_sig & FLASH_SIG_B2_MASK) != FLASH_SIG_B2)
|
|
flash_error("Incorrect LFS build type");
|
|
if ((fh->flash_sig & ~FLASH_SIG_ABSOLUTE) != FLASH_SIG)
|
|
flash_error("incorrect LFS header signature");
|
|
if (fh->flash_size > flashSize)
|
|
flash_error("LFS Image too big for configured LFS region");
|
|
if ((fh->flash_size & 0x3) ||
|
|
fh->flash_size > flashSize ||
|
|
out->flagsLen != 1 + (out->flashLen/WORDSIZE - 1) / BITS_PER_WORD)
|
|
flash_error("LFS length mismatch");
|
|
out->flags = luaM_newvector(out->L, out->flagsLen, unsigned);
|
|
}
|
|
|
|
/* update running CRC */
|
|
out->crc = uzlib_crc32(out->block[0], len, out->crc);
|
|
|
|
/* copy out any flag vector */
|
|
if (out->ndx > out->flashLen) {
|
|
int start = out->flashLen - (out->ndx - len);
|
|
if (start < 0) start = 0;
|
|
memcpy(out->flags + out->flagsNdx, out->block[0]->byte + start, len - start);
|
|
out->flagsNdx += (len -start) / WORDSIZE; /* flashLen and len are word aligned */
|
|
}
|
|
}
|
|
|
|
|
|
void procSecondPass (void) {
|
|
/*
|
|
* The length rules are different for the second pass since this only processes
|
|
* upto the flashLen and not the full image. This also works in word units.
|
|
* (We've already validated these are word multiples.)
|
|
*/
|
|
int i, len = (out->ndx > out->flashLen) ?
|
|
(out->flashLen % WRITE_BLOCKSIZE) / WORDSIZE :
|
|
WRITE_BLOCKSIZE / WORDSIZE;
|
|
uint32_t *buf = (uint32_t *) out->buffer.byte;
|
|
uint32_t flags = 0;
|
|
/*
|
|
* Relocate all the addresses tagged in out->flags. This can't be done in
|
|
* place because the out->blocks are still in use as dictionary content so
|
|
* first copy the block to a working buffer and do the relocation in this.
|
|
*/
|
|
memcpy(out->buffer.byte, out->block[0]->byte, WRITE_BLOCKSIZE);
|
|
for (i=0; i<len; i++,flags>>=1 ) {
|
|
if ((i&31)==0)
|
|
flags = out->flags[out->flagsNdx++];
|
|
if (flags&1)
|
|
buf[i] = WORDSIZE*buf[i] + cast(uint32_t, flashAddr);
|
|
}
|
|
/*
|
|
* On first block, set the flash_sig has the in progress bit set and this
|
|
* is not cleared until end.
|
|
*/
|
|
if (out->ndx <= WRITE_BLOCKSIZE)
|
|
buf[0] = out->flash_sig | FLASH_SIG_IN_PROGRESS;
|
|
|
|
flashBlock(buf, len*WORDSIZE);
|
|
|
|
if (out->ndx >= out->flashLen) {
|
|
/* we're done so disable CB and rewrite flash sig to complete flash */
|
|
flashSetPosition(0);
|
|
flashBlock(&out->flash_sig, WORDSIZE);
|
|
out->fullBlkCB = NULL;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* loadLFS)() is protected called from luaN_reload_reboot so that it can recover
|
|
* from out of memory and other thrown errors. loadLFSgc() GCs any resources.
|
|
*/
|
|
static int loadLFS (lua_State *L) {
|
|
const char *fn = cast(const char *, lua_touserdata(L, 1));
|
|
int i, res;
|
|
uint32_t crc;
|
|
|
|
/* Allocate and zero in and out structures */
|
|
|
|
in = NULL; out = NULL;
|
|
in = luaM_new(L, struct INPUT);
|
|
memset(in, 0, sizeof(*in));
|
|
out = luaM_new(L, struct OUTPUT);
|
|
memset(out, 0, sizeof(*out));
|
|
out->L = L;
|
|
out->fullBlkCB = procFirstPass;
|
|
out->crc = ~0;
|
|
|
|
/* Open LFS image/ file, read unpacked length from last 4 byte and rewind */
|
|
if (!(in->fd = vfs_open(fn, "r")))
|
|
flash_error("LFS image file not found");
|
|
in->len = vfs_size(in->fd);
|
|
if (in->len <= 200 || /* size of an empty luac output */
|
|
vfs_lseek(in->fd, in->len-4, VFS_SEEK_SET) != in->len-4 ||
|
|
vfs_read(in->fd, &out->len, sizeof(unsigned)) != sizeof(unsigned))
|
|
flash_error("read error on LFS image file");
|
|
vfs_lseek(in->fd, 0, VFS_SEEK_SET);
|
|
|
|
/* Allocate the out buffers */
|
|
for(i = 0; i <= WRITE_BLOCKS; i++)
|
|
out->block[i] = luaM_new(L, outBlock);
|
|
|
|
/* first inflate pass */
|
|
if (uzlib_inflate (get_byte, put_byte, recall_byte,
|
|
in->len, &crc, &in->inflate_state) < 0)
|
|
flash_error("read error on LFS image file");
|
|
|
|
if (crc != ~out->crc)
|
|
flash_error("checksum error on LFS image file");
|
|
|
|
out->fullBlkCB = procSecondPass;
|
|
out->flagsNdx = 0;
|
|
out->ndx = 0;
|
|
in->bytesRead = in->left = 0;
|
|
/*
|
|
* Once we have completed the 1st pass then the LFS image has passed the
|
|
* basic signature, crc and length checks, so now we can reset the counts
|
|
* to do the actual write to flash on the second pass.
|
|
*/
|
|
vfs_lseek(in->fd, 0, VFS_SEEK_SET);
|
|
flashErase(0,(out->flashLen - 1)/FLASH_PAGE_SIZE);
|
|
flashSetPosition(0);
|
|
|
|
if ((res = uzlib_inflate(get_byte, put_byte, recall_byte,
|
|
in->len, &crc, &in->inflate_state)) != UZLIB_DONE) {
|
|
const char *err[] = {"Data_error during decompression",
|
|
"Chksum_error during decompression",
|
|
"Dictionary error during decompression",
|
|
"Memory_error during decompression"};
|
|
flash_error(err[UZLIB_DATA_ERROR - res]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int loadLFSgc (lua_State *L) {
|
|
int i;
|
|
if (out) {
|
|
for (i = 0; i < WRITE_BLOCKS; i++)
|
|
if (out->block[i])
|
|
luaM_free(L, out->block[i]);
|
|
if (out->flags)
|
|
luaM_freearray(L, out->flags, out->flagsLen, uint32_t);
|
|
luaM_free(L, out);
|
|
}
|
|
if (in) {
|
|
if (in->fd)
|
|
vfs_close(in->fd);
|
|
luaM_free(L, in);
|
|
}
|
|
return 0;
|
|
}
|