]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-lua: Add input & output stream wrappers
authorAki Tuomi <aki.tuomi@open-xchange.com>
Tue, 15 Apr 2025 10:48:18 +0000 (13:48 +0300)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Thu, 24 Apr 2025 07:03:55 +0000 (07:03 +0000)
src/lib-lua/Makefile.am
src/lib-lua/dlua-iostream.c [new file with mode: 0644]
src/lib-lua/dlua-script-private.h
src/lib-lua/dlua-script.h
src/lib-lua/test-io-lua.c [new file with mode: 0644]
src/lib-lua/test-io-lua.lua [new file with mode: 0644]

index 6a7be9e2625d0a904707a660ba4d4a99fdedc3aa..8e89a64729d221da023105b0b557bdaa24f9a612 100644 (file)
@@ -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 (file)
index 0000000..15fde5b
--- /dev/null
@@ -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;
+};
+
index 2b0b5ddd6600037eedab3f80c85d9f35b92fcf99..af5bc6c8fb6c67a28ae7a14c14187fe07b5aae76 100644 (file)
@@ -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);
 
index 5290e06365bd293fbc685ea944efde6f733055da..38bbdf57a6ebc00005b5111706b1f0c854a43a4e 100644 (file)
@@ -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 (file)
index 0000000..7ceb3f9
--- /dev/null
@@ -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 (file)
index 0000000..cdee94f
--- /dev/null
@@ -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<nl>", l2 == "line2\n")
+  test_assert("l3 == line3", l3 == "line3")
+  test_assert("l4 == line4<nl>", l4 == "line4\n")
+  l1 = is:read()
+  l2 = is:read()
+  test_assert("l1 == hello<nl>", l1 == "hello\n")
+  test_assert("l2 == world<nl>", 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<nl>", l1 == "hello\n")
+  test_assert("l2 == world<nl>", 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