/*
 * wslua_file.c
 *
 * Wireshark's interface to the Lua Programming Language
 * for custom file format reading/writing.
 *
 * (c) 2014, Hadriel Kaplan <hadrielk@yahoo.com>
 *
 * Wireshark - Network traffic analyzer
 * By Gerald Combs <gerald@wireshark.org>
 * Copyright 1998 Gerald Combs
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */
#include "config.h"
#define WS_LOG_DOMAIN LOG_DOMAIN_WSLUA

#include "wslua_file_common.h"

#include <errno.h>
#include <wiretap/file_wrappers.h>

#define MAX_LINE_LENGTH            65536

/* WSLUA_MODULE File Custom File Format Reading And Writing

   The classes/functions defined in this section allow you to create your own
   custom Lua-based "capture" file reader, or writer, or both.
 */


WSLUA_CLASS_DEFINE(File,FAIL_ON_NULL_OR_EXPIRED("File"));
/*
    A `File` object, passed into Lua as an argument by FileHandler callback
    functions (e.g., `read_open`, `read`, `write`, etc.).  This behaves similarly to the
    Lua `io` library's `file` object, returned when calling `io.open()`, *except*
    in this case you cannot call `file:close()`, `file:open()`, nor `file:setvbuf()`,
    since Wireshark/TShark manages the opening and closing of files.
    You also cannot use the '`io`' library itself on this object, i.e. you cannot
    do `io.read(file, 4)`.  Instead, use this `File` with the object-oriented style
    calling its methods, i.e. `myfile:read(4)`. (see later example)

    The purpose of this object is to hide the internal complexity of how Wireshark
    handles files, and instead provide a Lua interface that is familiar, by mimicking
    the `io` library. The reason true/raw `io` files cannot be used is because Wireshark
    does many things under the hood, such as compress the file, or write to `stdout`,
    or various other things based on configuration/commands.

    When a `File` object is passed in through reading-based callback functions, such as
    `read_open()`, `read()`, and `read_close()`, then the File object's `write()` and `flush()`
    functions are not usable and will raise an error if used.

    When a `File` object is passed in through writing-based callback functions, such as
    `write_open()`, `write()`, and `write_close()`, then the File object's `read()` and `lines()`
    functions are not usable and will raise an error if used.

    Note: A `File` object should never be stored/saved beyond the scope of the callback function
    it is passed in to.

    For example:

    [source,lua]
    ----
    function myfilehandler.read_open(file, capture)
        local position = file:seek()

        -- read 24 bytes
        local line = file:read(24)

        -- do stuff

        -- it's not our file type, seek back (unnecessary but just to show it...)
        file:seek("set",position)

        -- return false because it's not our file type
        return false
    end
    ----
 */


/* a "File" object can be different things under the hood. It can either
   be a FILE_T from wtap struct, which it is during read operations, or it
   can be a wtap_dumper struct during write operations. A wtap_dumper struct
   has a WFILE_T member, but we can't only store its pointer here because
   dump operations need the whole thing to write out with. Ugh. */
File* push_File(lua_State* L, FILE_T ft) {
    File f = (File) g_malloc(sizeof(struct _wslua_file));
    f->file = ft;
    f->wdh = NULL;
    f->expired = false;
    return pushFile(L,f);
}

File* push_Wdh(lua_State* L, wtap_dumper *wdh) {
    File f = (File) g_malloc(sizeof(struct _wslua_file));
    f->file = (FILE_T)wdh->fh;
    f->wdh = wdh;
    f->expired = false;
    return pushFile(L,f);
}

static bool file_is_reader(File f) {
    return (f->wdh == NULL);
}

/* This internal function reads a number from the file, similar to Lua's io.read("*num").
 * In Lua this is done with a fscanf(file, "%lf", &double), but we can't use fscanf() since
 * this may be coming from a zip file and we need to use file_wrappers.c functions.
 * So we get a character at a time, building a buffer for fscanf.
 * XXX this isn't perfect - if just "2." exists in file, for example, it consumes it.
 */
#define WSLUA_MAXNUMBER2STR  32 /* 16 digits, sign, point, and \0 */
static int File_read_number (lua_State *L, FILE_T ft) {
    lua_Number d;
    char buff[WSLUA_MAXNUMBER2STR];
    int buff_end = 0;
    int c = -1;
    int num_digits = 0;
    bool has_decimal = false;

    c = file_peekc(ft);
    if (c == '+' || c == '-') {
        buff[buff_end++] = (char)c;
        /* make sure next char is a digit */
        c = file_peekc(ft);
        if (c < '0' || c > '9') {
            lua_pushnil(L);  /* "result" to be removed */
            return 0;  /* read fails */
        }
        /* eat the +/- */
        file_getc(ft);
    }

    while((c = file_peekc(ft)) > 0 && buff_end < (WSLUA_MAXNUMBER2STR-1)) {
        if (c >= '0' && c <= '9') {
            buff[buff_end++] = (char)c;
            num_digits++;
            file_getc(ft);
        }
        else if (!has_decimal && c == '.') {
            has_decimal = true;
            buff[buff_end++] = (char)c;
            file_getc(ft);
        }
        else break;
    }

    buff[buff_end] = '\0';

    if (buff_end > 0 && num_digits > 0 && sscanf(buff, "%lf", &d) == 1) {
        lua_pushnumber(L, d);
        return 1;
    }
    else {
        lua_pushnil(L);  /* "result" to be removed */
        return 0;  /* read fails */
    }
}

/**
 * Attempts to read one line from the file. The actual data read is pushed on
 * the stack (or nil on EOF).
 */
static int File_read_line(lua_State *L, FILE_T ft) {
    static char linebuff[MAX_LINE_LENGTH];
    int64_t pos_before = file_tell(ft);
    int length = 0;

    if (file_gets(linebuff, MAX_LINE_LENGTH, ft) == NULL) {
        /* No characters found, or error */
        /* *err = file_error(ft, err_info); */
        /* io.lines() and file:read() requires nil on EOF */
        lua_pushnil(L);
        return 0;
    }

    /* Set length (avoiding strlen()) */
    length = (int)(file_tell(ft) - pos_before);

    /* ...but don't want to include newline in line length */
    if (length > 0 && linebuff[length-1] == '\n') {
        length--;
        /* Nor do we want '\r' (as will be written when log is created on windows) */
        if (length > 0 && linebuff[length - 1] == '\r') {
            length--;
        }
        linebuff[length] = '\0';
    }

    lua_pushlstring(L, linebuff, length);
    return 1;
}

/* This internal function reads X number of bytes from the file, same as `io.read(num)` in Lua.
 * Since we have to use file_wrappers.c, and an intermediate buffer, we read it in chunks
 * of 1024 bytes at a time. (or less if called with a smaller number)  To do that, we use
 * Lua's buffer manager to push it into Lua as those chunks, while ending up with one long
 * Lua string in the end.
 */
#define WSLUA_BUFFERSIZE 1024

/**
 * Reads some data and returns the number of bytes read.
 * The actual data (possibly an empty string) is pushed on the Lua stack.
 */
static int File_read_chars(lua_State *L, FILE_T ft, size_t n) {
    size_t rlen;  /* how much to read */
    size_t nr;  /* number of chars actually read */
    int    nri; /* temp number of chars read, as an int to handle -1 errors */
    char buff[WSLUA_BUFFERSIZE];  /* for file_read to write to, and we push into Lua */
    luaL_Buffer b;

    rlen = WSLUA_BUFFERSIZE;  /* try to read that much each time */
    luaL_buffinit(L, &b); /* initialize Lua buffer */

    do {
        if (rlen > n) rlen = n;  /* cannot read more than asked */
        nri = file_read(buff, (unsigned int)rlen, ft);
        if (nri < 1) break;
        nr = (size_t) nri;
        luaL_addlstring(&b, buff, nr);
        n -= nr;  /* still have to read `n' chars */
    } while (n > 0 && nr == rlen);  /* until end of count or eof */

    luaL_pushresult(&b);  /* close buffer */

    return (n == 0 || lua_rawlen(L, -1) > 0);
}

/* returns nil if EOF, else an empty string - this is what Lua does too for this case */
static int File_test_eof(lua_State *L, FILE_T ft) {
    if (file_eof(ft)) {
        lua_pushnil(L);
    }
    else {
        lua_pushlstring(L, "", 0);
    }
    return 1;
}

static int pushresult (lua_State *L, int i, const char *filename) {
  int en = errno;  /* calls to Lua API may change this value, so we save it */
  if (i) {
    lua_pushboolean(L, 1);
    return 1;
  }
  else {
    lua_pushnil(L);
    if (filename)
      lua_pushfstring(L, "%s: %s", filename, g_strerror(en));
    else
      lua_pushfstring(L, "%s", g_strerror(en));
    lua_pushinteger(L, en);
    return 3;
  }
}

WSLUA_METHOD File_read(lua_State* L) {
    /* Reads from the File, similar to Lua's `file:read()`.  See Lua 5.x ref manual for `file:read()`. */
    File f = shiftFile(L,1);
    int nargs = lua_gettop(L);
    int success;
    int n = 1;
    FILE_T ft = NULL;

    if (!f || !f->file) {
        return 0;
    }

    /* shiftFile() doesn't verify things like expired */
    if (f->expired) {
        ws_warning("Error in File read: Lua File has expired");
        return 0;
    }

    if (!file_is_reader(f)) {
        ws_warning("Error in File read: this File object instance is for writing only");
        return 0;
    }

    ft = f->file;

    /* file_clearerr(ft); */
    if (nargs == 0) {  /* no arguments? */
        success = File_read_line(L, ft);
        n = 2;  /* to return 1 result */
    }
    else {  /* ensure stack space for all results and Lua */
        luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments");
        success = 1;
        for (n = 1; nargs-- && success; n++) {
            if (lua_type(L, n) == LUA_TNUMBER) {
                size_t l = (size_t)lua_tointeger(L, n);
                success = (l == 0) ? File_test_eof(L, ft) : File_read_chars(L, ft, l);
            }
            else {
                const char *p = lua_tostring(L, n);
                if (!p) return luaL_argerror(L, n, "invalid format argument");
                luaL_argcheck(L, p[0] == '*', n, "invalid option");
                switch (p[1]) {
                    case 'n':  /* number */
                        success = File_read_number(L, ft);
                        break;
                    case 'l':  /* line */
                        success = File_read_line(L, ft);
                        break;
                    case 'a':  /* file, read everything */
                        File_read_chars(L, ft, ~((size_t)0));  /* read MAX_SIZE_T chars */
                        success = 1; /* always success */
                        break;
                    default:
                        return luaL_argerror(L, n, "invalid format");
                }
            }
        }
    }
    if (file_error(ft, NULL))
        return pushresult(L, 0, NULL);
    if (!success) {
        lua_pop(L, 1);  /* remove last result */
        lua_pushnil(L);  /* push nil instead */
    }
    return n - 1;
}

WSLUA_METHOD File_seek(lua_State* L) {
    /* Seeks in the File, similar to Lua's `file:seek()`.  See Lua 5.x ref manual for `file:seek()`. */
    static const int mode[] = { SEEK_SET, SEEK_CUR, SEEK_END };
    static const char *const modenames[] = {"set", "cur", "end", NULL};
    File f = checkFile(L,1);
    int op = luaL_checkoption(L, 2, "cur", modenames);
#if LUA_VERSION_NUM >= 503
    int64_t offset = (int64_t)luaL_optinteger(L, 3, 0);
#else
    int64_t offset = (int64_t) luaL_optlong(L, 3, 0);
#endif
    int err;


    if (file_is_reader(f)) {
        offset = file_seek(f->file, offset, mode[op], &err);

        if (offset < 0) {
            lua_pushnil(L);  /* error */
            lua_pushstring(L, wtap_strerror(err));
            return 2;
        }

        lua_pushinteger(L, (lua_Integer)(file_tell(f->file)));
    }
    else {
        offset = wtap_dump_file_seek(f->wdh, offset, mode[op], &err);

        if (offset < 0) {
            lua_pushnil(L);  /* error */
            lua_pushstring(L, wtap_strerror(err));
            return 2;
        }

        offset = wtap_dump_file_tell(f->wdh, &err);

        if (offset < 0) {
            lua_pushnil(L);  /* error */
            lua_pushstring(L, wtap_strerror(err));
            return 2;
        }

        lua_pushinteger(L, (lua_Integer)(offset));
    }

    WSLUA_RETURN(1); /* The current file cursor position as a number. */
}

static int File_lines_iterator(lua_State* L) {
    FILE_T ft = *(FILE_T *)lua_touserdata(L, lua_upvalueindex(1));
    int success;

    if (ft == NULL)
        return luaL_error(L, "Error getting File handle for lines iterator");

    success = File_read_line(L, ft);

    /* if (ferror(ft))
        return luaL_error(L, "%s", g_strerror(errno));
    */
    return success;
}

WSLUA_METHOD File_lines(lua_State* L) {
    /* Lua iterator function for retrieving ASCII File lines, similar to Lua's `file:lines()`.  See Lua 5.x ref manual for `file:lines()`. */
    File f = checkFile(L,1);
    FILE_T ft = NULL;

    if (!f->file)
        return luaL_error(L, "Error getting File handle for lines");

    if (!file_is_reader(f)) {
        ws_warning("Error in File read: this File object instance is for writing only");
        return 0;
    }

    ft = f->file;

    lua_pushlightuserdata(L, ft);
    lua_pushcclosure(L, File_lines_iterator, 1);

    return 1;
}

/* yeah this function is a little weird, but I'm mimicking Lua's actual code for io:write() */
WSLUA_METHOD File_write(lua_State* L) {
    /* Writes to the File, similar to Lua's file:write().  See Lua 5.x ref manual for file:write(). */
    File f = checkFile(L,1);
    int arg = 2;                   /* beginning index for arguments */
    int nargs = lua_gettop(L) - 1;
    int status = true;
    int err = 0;

    if (!f->wdh) {
        ws_warning("Error in File read: this File object instance is for reading only");
        return 0;
    }

    lua_pushvalue(L, 1);  /* push File at the stack top (to be returned) */

    for (; nargs--; arg++) {
        size_t len;
        const char *s = luaL_checklstring(L, arg, &len);
        status = wtap_dump_file_write(f->wdh, s, len, &err);
        if (!status) break;
        f->wdh->bytes_dumped += len;
    }

    if (!status) {
        lua_pop(L,1); /* pop the extraneous File object */
        lua_pushnil(L);
        lua_pushfstring(L, "File write error: %s", g_strerror(err));
        lua_pushinteger(L, err);
        return 3;
    }

    return 1;  /* File object already on stack top */
}

WSLUA_METAMETHOD File__tostring(lua_State* L) {
    /* Returns a short label of the form `File: mode=<mode>` where
       <mode> is `reader` or `writer`, with `(expired)` appended
       once the FileHandler callback that produced it has returned.
       The previous form exposed the raw C pointer as `<ptr>` which
       added no useful information. */
    File f = toFile(L,1);

    if (!f) {
        lua_pushstring(L, "File: (null)");
        WSLUA_RETURN(1);
    }

    const char *mode = f->wdh ? "writer" : "reader";
    if (f->expired) {
        lua_pushfstring(L, "File: mode=%s (expired)", mode);
    } else {
        lua_pushfstring(L, "File: mode=%s", mode);
    }

    WSLUA_RETURN(1); /* A short label identifying the file. */
}

/* We free the struct we malloc'ed, but not the FILE_T/dumper in it of course */
static int File__gc(lua_State* L) {
    File f = toFile(L,1);
    g_free(f);
    return 0;
}

/* WSLUA_ATTRIBUTE File_compressed RO Whether the File is compressed or not.

    See `wtap_encaps` for available types.  Set to `wtap_encaps.PER_PACKET` if packets can
    have different types, then later set `FrameInfo.encap` for each packet during read()/seek_read(). */
static int File_get_compressed(lua_State* L) {
    File f = checkFile(L,1);

    if (file_is_reader(f)) {
        lua_pushboolean(L, file_iscompressed(f->file));
    } else {
        lua_pushboolean(L, f->wdh->compression_type != WS_FILE_UNCOMPRESSED);
    }
    return 1;
}

WSLUA_ATTRIBUTES File_attributes[] = {
    WSLUA_ATTRIBUTE_ROREG(File,compressed),
    { NULL, NULL, NULL }
};

WSLUA_METHODS File_methods[] = {
    WSLUA_CLASS_FNREG(File,lines),
    WSLUA_CLASS_FNREG(File,read),
    WSLUA_CLASS_FNREG(File,seek),
    WSLUA_CLASS_FNREG(File,write),
    { NULL, NULL }
};

WSLUA_META File_meta[] = {
    WSLUA_CLASS_MTREG(File,tostring),
    { NULL, NULL }
};

int File_register(lua_State* L) {
    WSLUA_REGISTER_CLASS_WITH_ATTRS(File);
    return 0;
}


/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 4
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=4 tabstop=8 expandtab:
 * :indentSize=4:tabSize=8:noTabs=true:
 */
