--- /dev/null
+/* 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;
+};
+
--- /dev/null
+/* 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);
+}