From: Aki Tuomi Date: Tue, 15 Apr 2025 10:48:18 +0000 (+0300) Subject: lib-lua: Add input & output stream wrappers X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b56f96b82cd5eccd5099a29a811bc693d4d1c86b;p=thirdparty%2Fdovecot%2Fcore.git lib-lua: Add input & output stream wrappers --- diff --git a/src/lib-lua/Makefile.am b/src/lib-lua/Makefile.am index 6a7be9e262..8e89a64729 100644 --- a/src/lib-lua/Makefile.am +++ b/src/lib-lua/Makefile.am @@ -21,7 +21,8 @@ libdlua_la_SOURCES = \ dlua-compat.c \ dlua-resume.c \ dlua-table.c \ - dlua-thread.c + dlua-thread.c \ + dlua-iostream.c test_programs = test-lua test-lua-http-client @@ -33,7 +34,8 @@ WITH_YIELDS_LUA += \ ../lib-dns-client/libdns_lua.la test_programs += \ test-dict-lua \ - test-dns-lua + test-dns-lua \ + test-io-lua endif libdlua_la_LIBADD = $(WITH_YIELDS_LUA) $(LUA_LIBS) @@ -60,7 +62,8 @@ pkginc_libdir=$(pkgincludedir) pkginc_lib_HEADERS = $(headers) EXTRA_DIST = \ - test-lua-http-client.lua + test-lua-http-client.lua \ + test-io-lua.lua noinst_PROGRAMS = $(test_programs) @@ -80,6 +83,10 @@ test_dns_lua_SOURCES = test-dns-lua.c test_dns_lua_LDADD = libdlua.la $(LIBDOVECOT) $(LUA_LIBS) test_dns_lua_DEPENDENCIES = libdlua.la $(LIBDOVECOT_DEPS) +test_io_lua_SOURCES = test-io-lua.c +test_io_lua_LDADD = libdlua.la $(LIBDOVECOT) $(LUA_LIBS) +test_io_lua_DEPENDENCIES = libdlua.la $(LIBDOVECOT_DEPS) + test_lua_http_client_SOURCES = test-lua-http-client.c test_lua_http_client_LDADD = libdlua.la $(LIBDOVECOT) $(test_libs_ssl) $(LUA_LIBS) test_lua_http_client_DEPENDENCIES = libdlua.la $(LIBDOVECOT_DEPS) diff --git a/src/lib-lua/dlua-iostream.c b/src/lib-lua/dlua-iostream.c new file mode 100644 index 0000000000..15fde5b6a9 --- /dev/null +++ b/src/lib-lua/dlua-iostream.c @@ -0,0 +1,371 @@ +/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "istream.h" +#include "ostream.h" +#include "str.h" +#include "dlua-script-private.h" +#include "lua.h" +#include "lauxlib.h" + +#define DOVECOT_FILEHANDLE "struct dlua_iostream*" +#define DLUA_DOVECOT_IO "io" +#define MAXARGLINE 250 + +struct dlua_iostream { + struct luaL_Stream stream; + struct istream *is; + struct ostream *os; + bool input:1; +}; + +static int dlua_io_close(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + stream->stream.closef = NULL; + if (stream->input) { + i_stream_unref(&stream->is); + } else { + o_stream_unref(&stream->os); + } + return 0; +} + +static int dlua_io_gc(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + if (stream->stream.closef != NULL) + dlua_io_close(L); + i_assert(stream->stream.closef == NULL); + return 0; +} + +static int dlua_io_tostring(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + if (stream->stream.closef == NULL) + lua_pushliteral(L, "file (closed)"); + else if (stream->input) + lua_pushstring(L, i_stream_get_name(stream->is)); + else + lua_pushstring(L, o_stream_get_name(stream->os)); + return 1; +} + +static int dlua_o_write(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + if (stream->stream.closef == NULL) + return luaL_error(L, "Cannot write to closed file"); + if (stream->input) + return luaL_error(L, "Cannot write to input stream"); + + struct const_iovec vec; + vec.iov_base = luaL_tolstring(L, 2, &vec.iov_len); + ssize_t ret = o_stream_sendv(stream->os, &vec, 1); + + if (ret < 0) { + errno = stream->os->stream_errno; + return luaL_fileresult(L, 0, o_stream_get_name(stream->os)); + } + return 0; +} + +static int dlua_o_flush(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + if (stream->stream.closef == NULL) + return luaL_error(L, "Cannot flush closed file"); + if (stream->input) + return luaL_error(L, "Cannot flush input stream"); + + ssize_t ret = o_stream_flush(stream->os); + + if (ret < 0) { + errno = stream->os->stream_errno; + return luaL_fileresult(L, 0, o_stream_get_name(stream->os)); + } + return 0; +} + +static int dlua_io_setvbuf(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + size_t max_size = lua_tonumber(L, 2); + + if (stream->stream.closef == NULL) + return luaL_error(L, "Cannot change buffer size on closed file"); + + if (stream->input) + i_stream_set_max_buffer_size(stream->is, max_size); + else + o_stream_set_max_buffer_size(stream->os, max_size); + return 0; +} + +static bool dlua_read_line(lua_State *L, struct dlua_iostream *stream, bool nl) +{ + /* check available data */ + string_t *str = t_str_new(32); + + /* We don't want to use i_stream_read_next_line() because next call might + * be something else, like reading just n bytes from the stream. */ + while (i_stream_have_bytes_left(stream->is)) { + size_t size; + const unsigned char *data = i_stream_get_data(stream->is, &size); + const unsigned char *ptr = memchr(data, '\n', size); + + if (ptr != NULL) { + ptr++; + /* check that there is no embedded NUL */ + const unsigned char *ptr2 = memchr(data, '\0', ptr - data); + if (ptr2 != NULL) + ptr = ptr2; + size = ptr - data; + } else { + /* stop at first NUL */ + const unsigned char *ptr2 = memchr(data, '\0', size); + if (ptr2 != NULL) { + ptr = ptr2; + size = ptr - data; + } + } + + str_append_data(str, data, size); + /* consume read data from stream */ + i_stream_skip(stream->is, size); + + /* end of read */ + if (ptr != NULL) + break; + + i_stream_read(stream->is); + } + + /* Nothing read, fail */ + if (str->used == 0) + return FALSE; + + /* Check if we want to add or remove newline */ + const char *ptr = strchr(str_c(str), '\n'); + if (ptr == NULL && nl) { + str_append_c(str, '\n'); + } else if (ptr != NULL && !nl) { + str_truncate(str, str_len(str) - 1); + } + + lua_pushstring(L, str_c(str)); + return TRUE; +} + +static bool dlua_read_bytes(lua_State *L, struct dlua_iostream *stream, size_t bytes) +{ + size_t size; + const unsigned char *data; + string_t *str = t_str_new(32); + + while (bytes > 0 && i_stream_read_more(stream->is, &data, &size) > 0) { + if (bytes < size) + size = bytes; + bytes -= size; + str_append_data(str, data, size); + i_stream_skip(stream->is, size); + } + + lua_pushlstring(L, str->data, str->used); + + return TRUE; +} + +/* Adapted from g_read() in lua */ +static int dlua_i_read_common(lua_State *L, struct dlua_iostream *stream, int first) +{ + int nargs = lua_gettop(L) - 1; + bool success; + int n; + (void)i_stream_read(stream->is); + + if (nargs == 0) { + success = dlua_read_line(L, stream, TRUE); + n = first + 1; + } else { + luaL_checkstack(L, nargs+LUA_MINSTACK, "too many arguments"); + success = TRUE; + for (n = first; nargs-- > 0 && success; n++) { + if (lua_type(L, n) == LUA_TNUMBER) { + size_t l = (size_t)luaL_checkinteger(L, n); + success = dlua_read_bytes(L, stream, l); + } else { + const char *p = luaL_checkstring(L, n); + /* skip optional '*' (for compatibility) */ + if (*p == '*') + p++; + switch (*p) { + case 'n': /* number */ + return luaL_argerror(L, n, "unsupported format"); + case 'l': /* line */ + success = dlua_read_line(L, stream, FALSE); + break; + case 'L': /* line with end-of-line */ + success = dlua_read_line(L, stream, TRUE); + break; + case 'a': /* read entire file */ + success = dlua_read_bytes(L, stream, SIZE_MAX); + break; + default: + return luaL_argerror(L, n, "invalid format"); + } + } + } + } + + if (stream->is->stream_errno != 0) { + errno = stream->is->stream_errno; + return luaL_fileresult(L, 0, i_stream_get_name(stream->is)); + } + + if (!success){ + lua_pop(L, 1); + lua_pushnil(L); + } + + return n - first; +} + +static int dlua_i_read(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + if (stream->stream.closef == NULL) + return luaL_error(L, "Cannot read closed file"); + if (!stream->input) + return luaL_error(L, "Cannot read from output stream"); + return dlua_i_read_common(L, stream, 2); +} + +static int dlua_i_seek(lua_State *L) +{ + static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END}; + static const char *const modenames[] = {"set", "cur", "end", NULL}; + + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, 1, DOVECOT_FILEHANDLE)); + if (stream->stream.closef == NULL) + return luaL_error(L, "Cannot seek closed file"); + if (!stream->input) + return luaL_error(L, "Cannot seek output stream"); + + int op = luaL_checkoption(L, 2, "cur", modenames); + lua_Integer p3 = luaL_optinteger(L, 3, 0); + off_t offset = (off_t)p3; + if ((lua_Integer)offset != p3) + return luaL_argerror(L, 3, "not an integer in proper range"); + + if (mode[op] == SEEK_CUR) { + offset += i_stream_get_absolute_offset(stream->is); + } else if (mode[op] == SEEK_END) { + return luaL_argerror(L, 2, "end is not supported"); + } + + i_stream_seek(stream->is, offset); + return 0; +} + +static int dlua_i_readline(lua_State *L) +{ + struct dlua_iostream *stream = + ((struct dlua_iostream*)luaL_checkudata(L, lua_upvalueindex(1), DOVECOT_FILEHANDLE)); + if (stream->stream.closef == NULL) + return luaL_error(L, "Cannot read closed file"); + if (!stream->input) + return luaL_error(L, "Cannot read from output stream"); + + int i; + int n = (int)lua_tointeger(L, lua_upvalueindex(2)); + lua_settop(L , 1); + luaL_checkstack(L, n, "too many arguments"); + for (i = 1; i <= n; i++) /* push arguments to 'g_read' */ + lua_pushvalue(L, lua_upvalueindex(3 + i)); + n = dlua_i_read_common(L, stream, 2); /* 'n' is number of results */ + i_assert(n > 0); + if (lua_toboolean(L, -n)) /* read at least one value? */ + return n; + if (n > 1) { + return luaL_error(L, "%s", lua_tostring(L, -n + 1)); + } + return 0; +} + +static int dlua_i_lines(lua_State *L) +{ + int n = lua_gettop(L) - 1; /* number of arguments to read */ + if (n > MAXARGLINE) + return luaL_argerror(L, MAXARGLINE + 2, "too many arguments"); + lua_pushinteger(L, n); /* number of arguments to read */ + lua_pushcclosure(L, dlua_i_readline, 2 + n); + return 1; +} + +static const luaL_Reg dovecot_io_methods[] = { + {NULL, NULL} +}; + +static const luaL_Reg flib[] = { + {"close", dlua_io_close}, + {"flush", dlua_o_flush}, + {"lines", dlua_i_lines}, + {"read", dlua_i_read}, + {"seek", dlua_i_seek}, + {"setvbuf", dlua_io_setvbuf}, + {"write", dlua_o_write}, + {"__gc", dlua_io_gc}, + {"__tostring", dlua_io_tostring}, + {NULL, NULL} +}; + +void dlua_dovecot_io_register(struct dlua_script *script) { + dlua_get_dovecot(script->L); + lua_newtable(script->L); + luaL_setfuncs(script->L, dovecot_io_methods, 0); + lua_setfield(script->L, -2, DLUA_DOVECOT_IO); + lua_pop(script->L, 1); + + luaL_newmetatable(script->L, DOVECOT_FILEHANDLE); /* create metatable for file handles */ + lua_pushvalue(script->L, -1); /* push metatable */ + lua_setfield(script->L, -2, "__index"); /* metatable.__index = metatable */ + luaL_setfuncs(script->L, flib, 0); /* file methods */ +}; + +int dlua_push_istream(struct dlua_script *script, struct istream *is) { + struct dlua_iostream *stream = + lua_newuserdata(script->L, sizeof(struct dlua_iostream)); + luaL_setmetatable(script->L, DOVECOT_FILEHANDLE); + stream->stream.f = NULL; + stream->stream.closef = dlua_io_close; + i_assert(!is->closed); + i_stream_ref(is); + stream->is = is; + stream->input = TRUE; + + return 1; +}; + +int dlua_push_ostream(struct dlua_script *script, struct ostream *os) { + struct dlua_iostream *stream = + lua_newuserdata(script->L, sizeof(struct dlua_iostream)); + luaL_setmetatable(script->L, DOVECOT_FILEHANDLE); + stream->stream.f = NULL; + stream->stream.closef = dlua_io_close; + i_assert(!os->closed); + o_stream_ref(os); + stream->os = os; + + return 1; +}; + diff --git a/src/lib-lua/dlua-script-private.h b/src/lib-lua/dlua-script-private.h index 2b0b5ddd66..af5bc6c8fb 100644 --- a/src/lib-lua/dlua-script-private.h +++ b/src/lib-lua/dlua-script-private.h @@ -100,6 +100,9 @@ void dlua_get_dovecot(lua_State *L); /* register 'http' methods to 'dovecot' */ void dlua_dovecot_http_register(struct dlua_script *script); +/* register 'file' methods to 'dovecot' */ +void dlua_dovecot_io_register(struct dlua_script *script); + /* assign values to table on idx */ void dlua_set_members(lua_State *L, const struct dlua_table_values *values, int idx); diff --git a/src/lib-lua/dlua-script.h b/src/lib-lua/dlua-script.h index 5290e06365..38bbdf57a6 100644 --- a/src/lib-lua/dlua-script.h +++ b/src/lib-lua/dlua-script.h @@ -37,4 +37,12 @@ void dlua_script_unref(struct dlua_script **_script); /* see if particular function is registered */ bool dlua_script_has_function(struct dlua_script *script, const char *fn); + +struct istream; +struct ostream; + +/* stream wrappers */ +int dlua_push_istream(struct dlua_script *script, struct istream *is); +int dlua_push_ostream(struct dlua_script *script, struct ostream *os); + #endif diff --git a/src/lib-lua/test-io-lua.c b/src/lib-lua/test-io-lua.c new file mode 100644 index 0000000000..7ceb3f9104 --- /dev/null +++ b/src/lib-lua/test-io-lua.c @@ -0,0 +1,86 @@ +/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */ + +#include "lib.h" +#include "buffer.h" +#include "str.h" +#include "istream.h" +#include "ostream.h" +#include "dlua-script-private.h" +#include "test-common.h" + +static unsigned int assert_count = 0; + +static int dlua_test_assert(lua_State *L) +{ + struct dlua_script *script = dlua_script_from_state(L); + const char *what = luaL_checkstring(script->L, 1); + bool cond = lua_toboolean(script->L, 2); + + if (!cond) { + lua_Debug ar; + i_zero(&ar); + (void)lua_getinfo(L, ">Sl", &ar); + test_assert_failed(what, ar.source, ar.currentline); + } + + assert_count++; + + return 0; +} + +static void test_io_lua(void) +{ + test_begin("io lua"); + buffer_t *buf = t_buffer_create(32); + struct ostream *os = test_ostream_create(buf); + struct dlua_script *script; + const char *error; + + if (dlua_script_create_file("test-io-lua.lua", &script, NULL, &error) < 0) + i_fatal("%s", error); + + dlua_dovecot_register(script); + dlua_dovecot_io_register(script); + dlua_register(script, "test_assert", dlua_test_assert); + + dlua_script_init(script, &error); + + dlua_push_ostream(script, os); + o_stream_unref(&os); + if (dlua_pcall(script->L, "test_write_ostream", 1, 0, &error) < 0) + i_fatal("%s", error); + test_assert_strcmp(str_c(buf), "hello, world"); + + struct istream *is = test_istream_create(str_c(buf)); + dlua_push_istream(script, is); + i_stream_unref(&is); + if (dlua_pcall(script->L, "test_read_simple_istream", 1, 0, &error) < 0) + i_fatal("%s", error); + is = test_istream_create_data("line1\nline2\nline3\nline4\0hello\nworld", 35); + i_stream_set_max_buffer_size(is, 1); + dlua_push_istream(script, is); + i_stream_unref(&is); + if (dlua_pcall(script->L, "test_read_many", 1, 0, &error) < 0) + i_fatal("%s", error); + is = test_istream_create_data("hello\0world\0\1\2\3\4\5", 17); + dlua_push_istream(script, is); + i_stream_unref(&is); + if (dlua_pcall(script->L, "test_read_bytes", 1, 0, &error) < 0) + i_fatal("%s", error); + + dlua_script_unref(&script); + + /* ensure all tests were actually ran */ + test_assert_ucmp(assert_count, ==, 19); + + test_end(); +} + +int main(void) +{ + static void (*const test_functions[])(void) = { + test_io_lua, + NULL + }; + return test_run(test_functions); +} diff --git a/src/lib-lua/test-io-lua.lua b/src/lib-lua/test-io-lua.lua new file mode 100644 index 0000000000..cdee94f780 --- /dev/null +++ b/src/lib-lua/test-io-lua.lua @@ -0,0 +1,46 @@ +-- Copyright (c) 2025 Dovecot authors, see the included COPYING files + +function test_write_ostream(os) + os:write("hello, world") +end + +function test_read_simple_istream(is) + local line = is:read() + test_assert("was able to read", line == "hello, world\n") + line = is:read() + test_assert("eof is NULL", line == nil) +end + +function test_read_many(is) + local l1, l2, l3, l4, _ = is:read('l','L','l','L', 1) + test_assert("l1 == line1", l1 == "line1") + test_assert("l2 == line2", l2 == "line2\n") + test_assert("l3 == line3", l3 == "line3") + test_assert("l4 == line4", l4 == "line4\n") + l1 = is:read() + l2 = is:read() + test_assert("l1 == hello", l1 == "hello\n") + test_assert("l2 == world", l2 == "world\n") + -- test seeking and line iterator + is:seek('set', 0) + local i = 1 + for line in is:lines() do + test_assert("line == line"..tostring(i), line == "line"..tostring(i).."\n") + i = i + 1 + end + test_assert("i == 5", i == 5) + is:read(1) + l1 = is:read() + l2 = is:read() + test_assert("l1 == hello", l1 == "hello\n") + test_assert("l2 == world", l2 == "world\n") +end + +function test_read_bytes(is) + local h,_,w = is:read(5,1,5) + local r = is:read('a') + test_assert("h == hello", h == "hello") + test_assert("w == world", w == "world") + test_assert("r == \\0\\1\\2\\3\\4\\5", r == "\0\1\2\3\4\5") + test_assert("#r==6", #r == 6) +end