diff options
Diffstat (limited to 'player/lua.c')
-rw-r--r-- | player/lua.c | 1341 |
1 files changed, 1341 insertions, 0 deletions
diff --git a/player/lua.c b/player/lua.c new file mode 100644 index 0000000..41fd520 --- /dev/null +++ b/player/lua.c @@ -0,0 +1,1341 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <string.h> +#include <strings.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <dirent.h> +#include <math.h> + +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> + +#include "osdep/io.h" + +#include "mpv_talloc.h" + +#include "common/common.h" +#include "options/m_property.h" +#include "common/msg.h" +#include "common/msg_control.h" +#include "common/stats.h" +#include "options/m_option.h" +#include "input/input.h" +#include "options/path.h" +#include "misc/bstr.h" +#include "misc/json.h" +#include "osdep/subprocess.h" +#include "osdep/timer.h" +#include "osdep/threads.h" +#include "stream/stream.h" +#include "sub/osd.h" +#include "core.h" +#include "command.h" +#include "client.h" +#include "libmpv/client.h" + +// List of builtin modules and their contents as strings. +// All these are generated from player/lua/*.lua +static const char * const builtin_lua_scripts[][2] = { + {"mp.defaults", +# include "player/lua/defaults.lua.inc" + }, + {"mp.assdraw", +# include "player/lua/assdraw.lua.inc" + }, + {"mp.options", +# include "player/lua/options.lua.inc" + }, + {"@osc.lua", +# include "player/lua/osc.lua.inc" + }, + {"@ytdl_hook.lua", +# include "player/lua/ytdl_hook.lua.inc" + }, + {"@stats.lua", +# include "player/lua/stats.lua.inc" + }, + {"@console.lua", +# include "player/lua/console.lua.inc" + }, + {"@auto_profiles.lua", +# include "player/lua/auto_profiles.lua.inc" + }, + {0} +}; + +// Represents a loaded script. Each has its own Lua state. +struct script_ctx { + const char *name; + const char *filename; + const char *path; // NULL if single file + lua_State *state; + struct mp_log *log; + struct mpv_handle *client; + struct MPContext *mpctx; + size_t lua_malloc_size; + lua_Alloc lua_allocf; + void *lua_alloc_ud; + struct stats_ctx *stats; +}; + +#if LUA_VERSION_NUM <= 501 +#define mp_cpcall lua_cpcall +#define mp_lua_len lua_objlen +#else +// Curse whoever had this stupid idea. Curse whoever thought it would be a good +// idea not to include an emulated lua_cpcall() even more. +static int mp_cpcall (lua_State *L, lua_CFunction func, void *ud) +{ + lua_pushcfunction(L, func); // doesn't allocate in 5.2 (but does in 5.1) + lua_pushlightuserdata(L, ud); + return lua_pcall(L, 1, 0, 0); +} +#define mp_lua_len lua_rawlen +#endif + +// Ensure that the given argument exists, even if it's nil. Can be used to +// avoid confusing the last missing optional arg with the first temporary value +// pushed to the stack. +static void mp_lua_optarg(lua_State *L, int arg) +{ + while (arg > lua_gettop(L)) + lua_pushnil(L); +} + +// autofree: avoid leaks if a lua-error occurs between talloc new/free. +// If a lua c-function does a new allocation (not tied to an existing context), +// and an uncaught lua-error occurs before "free" - the allocation is leaked. + +// autofree lua C function: same as lua_CFunction but with these differences: +// - It accepts an additional void* argument - a pre-initialized talloc context +// which it can use, and which is freed with its children once the function +// completes - regardless if a lua error occurred or not. If a lua error did +// occur then it's re-thrown after the ctx is freed. +// The stack/arguments/upvalues/return are the same as with lua_CFunction. +// - It's inserted into the lua VM using af_pushc{function,closure} instead of +// lua_pushc{function,closure}, which takes care of wrapping it with the +// automatic talloc allocation + lua-error-handling + talloc release. +// This requires using AF_ENTRY instead of FN_ENTRY at struct fn_entry. +// - The autofree overhead per call is roughly two additional plain lua calls. +// Typically that's up to 20% slower than plain new+free without "auto", +// and at most about twice slower - compared to bare new+free lua_CFunction. +// - The overhead of af_push* is one additional lua-c-closure with two upvalues. +typedef int (*af_CFunction)(lua_State *L, void *ctx); + +static void af_pushcclosure(lua_State *L, af_CFunction fn, int n); +#define af_pushcfunction(L, fn) af_pushcclosure((L), (fn), 0) + + +// add_af_dir, add_af_mpv_alloc take a valid DIR*/char* value respectively, +// and closedir/mpv_free it when the parent is freed. + +static void destruct_af_dir(void *p) +{ + closedir(*(DIR**)p); +} + +static void add_af_dir(void *parent, DIR *d) +{ + DIR **pd = talloc(parent, DIR*); + *pd = d; + talloc_set_destructor(pd, destruct_af_dir); +} + +static void destruct_af_mpv_alloc(void *p) +{ + mpv_free(*(char**)p); +} + +static void add_af_mpv_alloc(void *parent, char *ma) +{ + char **p = talloc(parent, char*); + *p = ma; + talloc_set_destructor(p, destruct_af_mpv_alloc); +} + + +// Perform the equivalent of mpv_free_node_contents(node) when tmp is freed. +static void steal_node_allocations(void *tmp, mpv_node *node) +{ + talloc_steal(tmp, node_get_alloc(node)); +} + +// lua_Alloc compatible. Serves only to track memory usage. This wraps the +// existing allocator, partly because luajit requires the use of its internal +// allocator on 64-bit platforms. +static void *mp_lua_alloc(void *ud, void *ptr, size_t osize, size_t nsize) +{ + struct script_ctx *ctx = ud; + + // Ah, what the fuck, screw whoever introduced this to Lua 5.2. + if (!ptr) + osize = 0; + + ptr = ctx->lua_allocf(ctx->lua_alloc_ud, ptr, osize, nsize); + if (nsize && !ptr) + return NULL; // allocation failed, so original memory left untouched + + ctx->lua_malloc_size = ctx->lua_malloc_size - osize + nsize; + stats_size_value(ctx->stats, "mem", ctx->lua_malloc_size); + + return ptr; +} + +static struct script_ctx *get_ctx(lua_State *L) +{ + lua_getfield(L, LUA_REGISTRYINDEX, "ctx"); + struct script_ctx *ctx = lua_touserdata(L, -1); + lua_pop(L, 1); + assert(ctx); + return ctx; +} + +static struct MPContext *get_mpctx(lua_State *L) +{ + return get_ctx(L)->mpctx; +} + +static int error_handler(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + + if (luaL_loadstring(L, "return debug.traceback('', 3)") == 0) { // e fn|err + lua_call(L, 0, 1); // e backtrace + const char *tr = lua_tostring(L, -1); + MP_WARN(ctx, "%s\n", tr ? tr : "(unknown)"); + } + lua_pop(L, 1); // e + + return 1; +} + +// Check client API error code: +// if err >= 0, push "true" to the stack, and return 1 +// if err < 0, push nil and then the error string to the stack, and return 2 +static int check_error(lua_State *L, int err) +{ + if (err >= 0) { + lua_pushboolean(L, 1); + return 1; + } + lua_pushnil(L); + lua_pushstring(L, mpv_error_string(err)); + return 2; +} + +static void add_functions(struct script_ctx *ctx); + +static void load_file(lua_State *L, const char *fname) +{ + struct script_ctx *ctx = get_ctx(L); + MP_DBG(ctx, "loading file %s\n", fname); + void *tmp = talloc_new(ctx); + // according to Lua manual chunkname should be '@' plus the filename + char *dispname = talloc_asprintf(tmp, "@%s", fname); + struct bstr s = stream_read_file(fname, tmp, ctx->mpctx->global, 100000000); + if (!s.start) + luaL_error(L, "Could not read file.\n"); + if (luaL_loadbuffer(L, s.start, s.len, dispname)) + lua_error(L); + lua_call(L, 0, 1); + talloc_free(tmp); +} + +static int load_builtin(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + char dispname[80]; + snprintf(dispname, sizeof(dispname), "@%s", name); + for (int n = 0; builtin_lua_scripts[n][0]; n++) { + if (strcmp(name, builtin_lua_scripts[n][0]) == 0) { + const char *script = builtin_lua_scripts[n][1]; + if (luaL_loadbuffer(L, script, strlen(script), dispname)) + lua_error(L); + lua_call(L, 0, 1); + return 1; + } + } + luaL_error(L, "builtin module '%s' not found\n", name); + return 0; +} + +// Execute "require " .. name +static void require(lua_State *L, const char *name) +{ + struct script_ctx *ctx = get_ctx(L); + MP_DBG(ctx, "loading %s\n", name); + // Lazy, but better than calling the "require" function manually + char buf[80]; + snprintf(buf, sizeof(buf), "require '%s'", name); + if (luaL_loadstring(L, buf)) + lua_error(L); + lua_call(L, 0, 0); +} + +// Push the table of a module. If it doesn't exist, it's created. +// The Lua script can call "require(module)" to "load" it. +static void push_module_table(lua_State *L, const char *module) +{ + lua_getglobal(L, "package"); // package + lua_getfield(L, -1, "loaded"); // package loaded + lua_remove(L, -2); // loaded + lua_getfield(L, -1, module); // loaded module + if (lua_isnil(L, -1)) { + lua_pop(L, 1); // loaded + lua_newtable(L); // loaded module + lua_pushvalue(L, -1); // loaded module module + lua_setfield(L, -3, module); // loaded module + } + lua_remove(L, -2); // module +} + +static int load_scripts(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *fname = ctx->filename; + + require(L, "mp.defaults"); + + if (fname[0] == '@') { + require(L, fname); + } else { + load_file(L, fname); + } + + lua_getglobal(L, "mp_event_loop"); // fn + if (lua_isnil(L, -1)) + luaL_error(L, "no event loop function\n"); + lua_call(L, 0, 0); // - + + return 0; +} + +static void fuck_lua(lua_State *L, const char *search_path, const char *extra) +{ + void *tmp = talloc_new(NULL); + + lua_getglobal(L, "package"); // package + lua_getfield(L, -1, search_path); // package search_path + bstr path = bstr0(lua_tostring(L, -1)); + char *newpath = talloc_strdup(tmp, ""); + + // Script-directory paths take priority. + if (extra) { + newpath = talloc_asprintf_append(newpath, "%s%s", + newpath[0] ? ";" : "", + mp_path_join(tmp, extra, "?.lua")); + } + + // Unbelievable but true: Lua loads .lua files AND dynamic libraries from + // the working directory. This is highly security relevant. + // Lua scripts are still supposed to load globally installed libraries, so + // try to get by by filtering out any relative paths. + while (path.len) { + bstr item; + bstr_split_tok(path, ";", &item, &path); + if (mp_path_is_absolute(item)) { + newpath = talloc_asprintf_append(newpath, "%s%.*s", + newpath[0] ? ";" : "", + BSTR_P(item)); + } + } + + lua_pushstring(L, newpath); // package search_path newpath + lua_setfield(L, -3, search_path); // package search_path + lua_pop(L, 2); // - + + talloc_free(tmp); +} + +static int run_lua(lua_State *L) +{ + struct script_ctx *ctx = lua_touserdata(L, -1); + lua_pop(L, 1); // - + + luaL_openlibs(L); + + // used by get_ctx() + lua_pushlightuserdata(L, ctx); // ctx + lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // - + + add_functions(ctx); // mp + + push_module_table(L, "mp"); // mp + + // "mp" is available by default, and no "require 'mp'" is needed + lua_pushvalue(L, -1); // mp mp + lua_setglobal(L, "mp"); // mp + + lua_pushstring(L, ctx->name); // mp name + lua_setfield(L, -2, "script_name"); // mp + + // used by pushnode() + lua_newtable(L); // mp table + lua_pushvalue(L, -1); // mp table table + lua_setfield(L, LUA_REGISTRYINDEX, "UNKNOWN_TYPE"); // mp table + lua_setfield(L, -2, "UNKNOWN_TYPE"); // mp + lua_newtable(L); // mp table + lua_pushvalue(L, -1); // mp table table + lua_setfield(L, LUA_REGISTRYINDEX, "MAP"); // mp table + lua_setfield(L, -2, "MAP"); // mp + lua_newtable(L); // mp table + lua_pushvalue(L, -1); // mp table table + lua_setfield(L, LUA_REGISTRYINDEX, "ARRAY"); // mp table + lua_setfield(L, -2, "ARRAY"); // mp + + lua_pop(L, 1); // - + + assert(lua_gettop(L) == 0); + + // Add a preloader for each builtin Lua module + lua_getglobal(L, "package"); // package + assert(lua_type(L, -1) == LUA_TTABLE); + lua_getfield(L, -1, "preload"); // package preload + assert(lua_type(L, -1) == LUA_TTABLE); + for (int n = 0; builtin_lua_scripts[n][0]; n++) { + lua_pushcfunction(L, load_builtin); // package preload load_builtin + lua_setfield(L, -2, builtin_lua_scripts[n][0]); + } + lua_pop(L, 2); // - + + assert(lua_gettop(L) == 0); + + fuck_lua(L, "path", ctx->path); + fuck_lua(L, "cpath", NULL); + assert(lua_gettop(L) == 0); + + // run this under an error handler that can do backtraces + lua_pushcfunction(L, error_handler); // errf + lua_pushcfunction(L, load_scripts); // errf fn + if (lua_pcall(L, 0, 0, -2)) { // errf [error] + const char *e = lua_tostring(L, -1); + MP_FATAL(ctx, "Lua error: %s\n", e ? e : "(unknown)"); + } + + return 0; +} + +static int load_lua(struct mp_script_args *args) +{ + int r = -1; + + struct script_ctx *ctx = talloc_ptrtype(NULL, ctx); + *ctx = (struct script_ctx) { + .mpctx = args->mpctx, + .client = args->client, + .name = mpv_client_name(args->client), + .log = args->log, + .filename = args->filename, + .path = args->path, + .stats = stats_ctx_create(ctx, args->mpctx->global, + mp_tprintf(80, "script/%s", mpv_client_name(args->client))), + }; + + stats_register_thread_cputime(ctx->stats, "cpu"); + + if (LUA_VERSION_NUM != 501 && LUA_VERSION_NUM != 502) { + MP_FATAL(ctx, "Only Lua 5.1 and 5.2 are supported.\n"); + goto error_out; + } + + lua_State *L = ctx->state = luaL_newstate(); + if (!L) { + MP_FATAL(ctx, "Could not initialize Lua.\n"); + goto error_out; + } + + // Wrap the internal allocator with our version that does accounting + ctx->lua_allocf = lua_getallocf(L, &ctx->lua_alloc_ud); + lua_setallocf(L, mp_lua_alloc, ctx); + + if (mp_cpcall(L, run_lua, ctx)) { + const char *err = "unknown error"; + if (lua_type(L, -1) == LUA_TSTRING) // avoid allocation + err = lua_tostring(L, -1); + MP_FATAL(ctx, "Lua error: %s\n", err); + goto error_out; + } + + r = 0; + +error_out: + if (ctx->state) + lua_close(ctx->state); + talloc_free(ctx); + return r; +} + +static int check_loglevel(lua_State *L, int arg) +{ + const char *level = luaL_checkstring(L, arg); + int n = mp_msg_find_level(level); + if (n >= 0) + return n; + luaL_error(L, "Invalid log level '%s'", level); + abort(); +} + +static int script_log(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + + int msgl = check_loglevel(L, 1); + + int last = lua_gettop(L); + lua_getglobal(L, "tostring"); // args... tostring + for (int i = 2; i <= last; i++) { + lua_pushvalue(L, -1); // args... tostring tostring + lua_pushvalue(L, i); // args... tostring tostring args[i] + lua_call(L, 1, 1); // args... tostring str + const char *s = lua_tostring(L, -1); + if (s == NULL) + return luaL_error(L, "Invalid argument"); + mp_msg(ctx->log, msgl, "%s%s", s, i > 0 ? " " : ""); + lua_pop(L, 1); // args... tostring + } + mp_msg(ctx->log, msgl, "\n"); + + return 0; +} + +static int script_find_config_file(lua_State *L) +{ + struct MPContext *mpctx = get_mpctx(L); + const char *s = luaL_checkstring(L, 1); + char *path = mp_find_config_file(NULL, mpctx->global, s); + if (path) { + lua_pushstring(L, path); + } else { + lua_pushnil(L); + } + talloc_free(path); + return 1; +} + +static int script_get_script_directory(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + if (ctx->path) { + lua_pushstring(L, ctx->path); + return 1; + } + return 0; +} + +static void pushnode(lua_State *L, mpv_node *node); + +static int script_raw_wait_event(lua_State *L, void *tmp) +{ + struct script_ctx *ctx = get_ctx(L); + + mpv_event *event = mpv_wait_event(ctx->client, luaL_optnumber(L, 1, 1e20)); + + struct mpv_node rn; + mpv_event_to_node(&rn, event); + steal_node_allocations(tmp, &rn); + + pushnode(L, &rn); // event + + // return event + return 1; +} + +static int script_request_event(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *event = luaL_checkstring(L, 1); + bool enable = lua_toboolean(L, 2); + // brute force event name -> id; stops working for events > assumed max + int event_id = -1; + for (int n = 0; n < 256; n++) { + const char *name = mpv_event_name(n); + if (name && strcmp(name, event) == 0) { + event_id = n; + break; + } + } + lua_pushboolean(L, mpv_request_event(ctx->client, event_id, enable) >= 0); + return 1; +} + +static int script_enable_messages(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *level = luaL_checkstring(L, 1); + int r = mpv_request_log_messages(ctx->client, level); + if (r == MPV_ERROR_INVALID_PARAMETER) + luaL_error(L, "Invalid log level '%s'", level); + return check_error(L, r); +} + +static int script_command(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *s = luaL_checkstring(L, 1); + + return check_error(L, mpv_command_string(ctx->client, s)); +} + +static int script_commandv(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + int num = lua_gettop(L); + const char *args[50]; + if (num + 1 > MP_ARRAY_SIZE(args)) + luaL_error(L, "too many arguments"); + for (int n = 1; n <= num; n++) { + const char *s = lua_tostring(L, n); + if (!s) + luaL_error(L, "argument %d is not a string", n); + args[n - 1] = s; + } + args[num] = NULL; + return check_error(L, mpv_command(ctx->client, args)); +} + +static int script_del_property(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *p = luaL_checkstring(L, 1); + + return check_error(L, mpv_del_property(ctx->client, p)); +} + +static int script_set_property(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *p = luaL_checkstring(L, 1); + const char *v = luaL_checkstring(L, 2); + + return check_error(L, mpv_set_property_string(ctx->client, p, v)); +} + +static int script_set_property_bool(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *p = luaL_checkstring(L, 1); + int v = lua_toboolean(L, 2); + + return check_error(L, mpv_set_property(ctx->client, p, MPV_FORMAT_FLAG, &v)); +} + +static bool is_int(double d) +{ + int64_t v = d; + return d == (double)v; +} + +static int script_set_property_number(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *p = luaL_checkstring(L, 1); + double d = luaL_checknumber(L, 2); + // If the number might be an integer, then set it as integer. The mpv core + // will (probably) convert INT64 to DOUBLE when setting, but not the other + // way around. + int res; + if (is_int(d)) { + res = mpv_set_property(ctx->client, p, MPV_FORMAT_INT64, &(int64_t){d}); + } else { + res = mpv_set_property(ctx->client, p, MPV_FORMAT_DOUBLE, &d); + } + return check_error(L, res); +} + +static void makenode(void *tmp, mpv_node *dst, lua_State *L, int t) +{ + luaL_checkstack(L, 6, "makenode"); + + if (t < 0) + t = lua_gettop(L) + (t + 1); + switch (lua_type(L, t)) { + case LUA_TNIL: + dst->format = MPV_FORMAT_NONE; + break; + case LUA_TNUMBER: { + double d = lua_tonumber(L, t); + if (is_int(d)) { + dst->format = MPV_FORMAT_INT64; + dst->u.int64 = d; + } else { + dst->format = MPV_FORMAT_DOUBLE; + dst->u.double_ = d; + } + break; + } + case LUA_TBOOLEAN: + dst->format = MPV_FORMAT_FLAG; + dst->u.flag = !!lua_toboolean(L, t); + break; + case LUA_TSTRING: { + size_t len = 0; + char *s = (char *)lua_tolstring(L, t, &len); + bool has_zeros = !!memchr(s, 0, len); + if (has_zeros) { + mpv_byte_array *ba = talloc_zero(tmp, mpv_byte_array); + *ba = (mpv_byte_array){talloc_memdup(tmp, s, len), len}; + dst->format = MPV_FORMAT_BYTE_ARRAY; + dst->u.ba = ba; + } else { + dst->format = MPV_FORMAT_STRING; + dst->u.string = talloc_strdup(tmp, s); + } + break; + } + case LUA_TTABLE: { + // Lua uses the same type for arrays and maps, so guess the correct one. + int format = MPV_FORMAT_NONE; + if (lua_getmetatable(L, t)) { // mt + lua_getfield(L, -1, "type"); // mt val + if (lua_type(L, -1) == LUA_TSTRING) { + const char *type = lua_tostring(L, -1); + if (strcmp(type, "MAP") == 0) { + format = MPV_FORMAT_NODE_MAP; + } else if (strcmp(type, "ARRAY") == 0) { + format = MPV_FORMAT_NODE_ARRAY; + } + } + lua_pop(L, 2); + } + if (format == MPV_FORMAT_NONE) { + // If all keys are integers, and they're in sequence, take it + // as an array. + int count = 0; + for (int n = 1; ; n++) { + lua_pushinteger(L, n); // n + lua_gettable(L, t); // t[n] + bool empty = lua_isnil(L, -1); // t[n] + lua_pop(L, 1); // - + if (empty) { + count = n - 1; + break; + } + } + if (count > 0) + format = MPV_FORMAT_NODE_ARRAY; + lua_pushnil(L); // nil + while (lua_next(L, t) != 0) { // key value + count--; + lua_pop(L, 1); // key + if (count < 0) { + lua_pop(L, 1); // - + format = MPV_FORMAT_NODE_MAP; + break; + } + } + } + if (format == MPV_FORMAT_NONE) + format = MPV_FORMAT_NODE_ARRAY; // probably empty table; assume array + mpv_node_list *list = talloc_zero(tmp, mpv_node_list); + dst->format = format; + dst->u.list = list; + if (format == MPV_FORMAT_NODE_ARRAY) { + for (int n = 0; ; n++) { + lua_pushinteger(L, n + 1); // n1 + lua_gettable(L, t); // t[n1] + if (lua_isnil(L, -1)) + break; + MP_TARRAY_GROW(tmp, list->values, list->num); + makenode(tmp, &list->values[n], L, -1); + list->num++; + lua_pop(L, 1); // - + } + lua_pop(L, 1); // - + } else { + lua_pushnil(L); // nil + while (lua_next(L, t) != 0) { // key value + MP_TARRAY_GROW(tmp, list->values, list->num); + MP_TARRAY_GROW(tmp, list->keys, list->num); + makenode(tmp, &list->values[list->num], L, -1); + if (lua_type(L, -2) != LUA_TSTRING) { + luaL_error(L, "key must be a string, but got %s", + lua_typename(L, lua_type(L, -2))); + } + list->keys[list->num] = talloc_strdup(tmp, lua_tostring(L, -2)); + list->num++; + lua_pop(L, 1); // key + } + } + break; + } + default: + // unknown type + luaL_error(L, "disallowed Lua type found: %s\n", lua_typename(L, t)); + } +} + +static int script_set_property_native(lua_State *L, void *tmp) +{ + struct script_ctx *ctx = get_ctx(L); + const char *p = luaL_checkstring(L, 1); + struct mpv_node node; + makenode(tmp, &node, L, 2); + int res = mpv_set_property(ctx->client, p, MPV_FORMAT_NODE, &node); + return check_error(L, res); + +} + +static int script_get_property_base(lua_State *L, void *tmp, int is_osd) +{ + struct script_ctx *ctx = get_ctx(L); + const char *name = luaL_checkstring(L, 1); + int type = is_osd ? MPV_FORMAT_OSD_STRING : MPV_FORMAT_STRING; + + char *result = NULL; + int err = mpv_get_property(ctx->client, name, type, &result); + if (err >= 0) { + add_af_mpv_alloc(tmp, result); + lua_pushstring(L, result); + return 1; + } else { + if (lua_isnoneornil(L, 2) && type == MPV_FORMAT_OSD_STRING) { + lua_pushstring(L, ""); + } else { + lua_pushvalue(L, 2); + } + lua_pushstring(L, mpv_error_string(err)); + return 2; + } +} + +static int script_get_property(lua_State *L, void *tmp) +{ + return script_get_property_base(L, tmp, 0); +} + +static int script_get_property_osd(lua_State *L, void *tmp) +{ + return script_get_property_base(L, tmp, 1); +} + +static int script_get_property_bool(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *name = luaL_checkstring(L, 1); + + int result = 0; + int err = mpv_get_property(ctx->client, name, MPV_FORMAT_FLAG, &result); + if (err >= 0) { + lua_pushboolean(L, !!result); + return 1; + } else { + lua_pushvalue(L, 2); + lua_pushstring(L, mpv_error_string(err)); + return 2; + } +} + +static int script_get_property_number(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + const char *name = luaL_checkstring(L, 1); + + // Note: the mpv core will (hopefully) convert INT64 to DOUBLE + double result = 0; + int err = mpv_get_property(ctx->client, name, MPV_FORMAT_DOUBLE, &result); + if (err >= 0) { + lua_pushnumber(L, result); + return 1; + } else { + lua_pushvalue(L, 2); + lua_pushstring(L, mpv_error_string(err)); + return 2; + } +} + +static void pushnode(lua_State *L, mpv_node *node) +{ + luaL_checkstack(L, 6, "pushnode"); + + switch (node->format) { + case MPV_FORMAT_STRING: + lua_pushstring(L, node->u.string); + break; + case MPV_FORMAT_INT64: + lua_pushnumber(L, node->u.int64); + break; + case MPV_FORMAT_DOUBLE: + lua_pushnumber(L, node->u.double_); + break; + case MPV_FORMAT_NONE: + lua_pushnil(L); + break; + case MPV_FORMAT_FLAG: + lua_pushboolean(L, node->u.flag); + break; + case MPV_FORMAT_NODE_ARRAY: + lua_newtable(L); // table + lua_getfield(L, LUA_REGISTRYINDEX, "ARRAY"); // table mt + lua_setmetatable(L, -2); // table + for (int n = 0; n < node->u.list->num; n++) { + pushnode(L, &node->u.list->values[n]); // table value + lua_rawseti(L, -2, n + 1); // table + } + break; + case MPV_FORMAT_NODE_MAP: + lua_newtable(L); // table + lua_getfield(L, LUA_REGISTRYINDEX, "MAP"); // table mt + lua_setmetatable(L, -2); // table + for (int n = 0; n < node->u.list->num; n++) { + lua_pushstring(L, node->u.list->keys[n]); // table key + pushnode(L, &node->u.list->values[n]); // table key value + lua_rawset(L, -3); + } + break; + case MPV_FORMAT_BYTE_ARRAY: + lua_pushlstring(L, node->u.ba->data, node->u.ba->size); + break; + default: + // unknown value - what do we do? + // for now, set a unique dummy value + lua_newtable(L); // table + lua_getfield(L, LUA_REGISTRYINDEX, "UNKNOWN_TYPE"); + lua_setmetatable(L, -2); // table + break; + } +} + +static int script_get_property_native(lua_State *L, void *tmp) +{ + struct script_ctx *ctx = get_ctx(L); + const char *name = luaL_checkstring(L, 1); + mp_lua_optarg(L, 2); + + mpv_node node; + int err = mpv_get_property(ctx->client, name, MPV_FORMAT_NODE, &node); + if (err >= 0) { + steal_node_allocations(tmp, &node); + pushnode(L, &node); + return 1; + } + lua_pushvalue(L, 2); + lua_pushstring(L, mpv_error_string(err)); + return 2; +} + +static mpv_format check_property_format(lua_State *L, int arg) +{ + if (lua_isnil(L, arg)) + return MPV_FORMAT_NONE; + const char *fmts[] = {"none", "native", "bool", "string", "number", NULL}; + switch (luaL_checkoption(L, arg, "none", fmts)) { + case 0: return MPV_FORMAT_NONE; + case 1: return MPV_FORMAT_NODE; + case 2: return MPV_FORMAT_FLAG; + case 3: return MPV_FORMAT_STRING; + case 4: return MPV_FORMAT_DOUBLE; + } + abort(); +} + +// It has a raw_ prefix, because there is a more high level API in defaults.lua. +static int script_raw_observe_property(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + uint64_t id = luaL_checknumber(L, 1); + const char *name = luaL_checkstring(L, 2); + mpv_format format = check_property_format(L, 3); + return check_error(L, mpv_observe_property(ctx->client, id, name, format)); +} + +static int script_raw_unobserve_property(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + uint64_t id = luaL_checknumber(L, 1); + lua_pushnumber(L, mpv_unobserve_property(ctx->client, id)); + return 1; +} + +static int script_command_native(lua_State *L, void *tmp) +{ + struct script_ctx *ctx = get_ctx(L); + mp_lua_optarg(L, 2); + struct mpv_node node; + struct mpv_node result; + makenode(tmp, &node, L, 1); + int err = mpv_command_node(ctx->client, &node, &result); + if (err >= 0) { + steal_node_allocations(tmp, &result); + pushnode(L, &result); + return 1; + } + lua_pushvalue(L, 2); + lua_pushstring(L, mpv_error_string(err)); + return 2; +} + +static int script_raw_command_native_async(lua_State *L, void *tmp) +{ + struct script_ctx *ctx = get_ctx(L); + uint64_t id = luaL_checknumber(L, 1); + struct mpv_node node; + makenode(tmp, &node, L, 2); + int res = mpv_command_node_async(ctx->client, id, &node); + return check_error(L, res); +} + +static int script_raw_abort_async_command(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + uint64_t id = luaL_checknumber(L, 1); + mpv_abort_async_command(ctx->client, id); + return 0; +} + +static int script_get_time(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + lua_pushnumber(L, mpv_get_time_us(ctx->client) / (double)(1000 * 1000)); + return 1; +} + +static int script_input_set_section_mouse_area(lua_State *L) +{ + struct MPContext *mpctx = get_mpctx(L); + + char *section = (char *)luaL_checkstring(L, 1); + int x0 = luaL_checkinteger(L, 2); + int y0 = luaL_checkinteger(L, 3); + int x1 = luaL_checkinteger(L, 4); + int y1 = luaL_checkinteger(L, 5); + mp_input_set_section_mouse_area(mpctx->input, section, x0, y0, x1, y1); + return 0; +} + +static int script_format_time(lua_State *L) +{ + double t = luaL_checknumber(L, 1); + const char *fmt = luaL_optstring(L, 2, "%H:%M:%S"); + char *r = mp_format_time_fmt(fmt, t); + if (!r) + luaL_error(L, "Invalid time format string '%s'", fmt); + lua_pushstring(L, r); + talloc_free(r); + return 1; +} + +static int script_get_wakeup_pipe(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + lua_pushinteger(L, mpv_get_wakeup_pipe(ctx->client)); + return 1; +} + +static int script_raw_hook_add(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + uint64_t ud = luaL_checkinteger(L, 1); + const char *name = luaL_checkstring(L, 2); + int pri = luaL_checkinteger(L, 3); + return check_error(L, mpv_hook_add(ctx->client, ud, name, pri)); +} + +static int script_raw_hook_continue(lua_State *L) +{ + struct script_ctx *ctx = get_ctx(L); + lua_Integer id = luaL_checkinteger(L, 1); + return check_error(L, mpv_hook_continue(ctx->client, id)); +} + +static int script_readdir(lua_State *L, void *tmp) +{ + // 0 1 2 3 + const char *fmts[] = {"all", "files", "dirs", "normal", NULL}; + const char *path = luaL_checkstring(L, 1); + int t = luaL_checkoption(L, 2, "normal", fmts); + DIR *dir = opendir(path); + if (!dir) { + lua_pushnil(L); + lua_pushstring(L, "error"); + return 2; + } + add_af_dir(tmp, dir); + lua_newtable(L); // list + char *fullpath = talloc_strdup(tmp, ""); + struct dirent *e; + int n = 0; + while ((e = readdir(dir))) { + char *name = e->d_name; + if (t) { + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + continue; + if (fullpath) + fullpath[0] = '\0'; + fullpath = talloc_asprintf_append(fullpath, "%s/%s", path, name); + struct stat st; + if (stat(fullpath, &st)) + continue; + if (!(((t & 1) && S_ISREG(st.st_mode)) || + ((t & 2) && S_ISDIR(st.st_mode)))) + continue; + } + lua_pushinteger(L, ++n); // list index + lua_pushstring(L, name); // list index name + lua_settable(L, -3); // list + } + return 1; +} + +static int script_file_info(lua_State *L) +{ + const char *path = luaL_checkstring(L, 1); + + struct stat statbuf; + if (stat(path, &statbuf) != 0) { + lua_pushnil(L); + lua_pushstring(L, "error"); + return 2; + } + + lua_newtable(L); // Result stat table + + const char * stat_names[] = { + "mode", "size", + "atime", "mtime", "ctime", NULL + }; + const lua_Number stat_values[] = { + statbuf.st_mode, + statbuf.st_size, + statbuf.st_atime, + statbuf.st_mtime, + statbuf.st_ctime + }; + + // Add all fields + for (int i = 0; stat_names[i]; i++) { + lua_pushnumber(L, stat_values[i]); + lua_setfield(L, -2, stat_names[i]); + } + + // Convenience booleans + lua_pushboolean(L, S_ISREG(statbuf.st_mode)); + lua_setfield(L, -2, "is_file"); + + lua_pushboolean(L, S_ISDIR(statbuf.st_mode)); + lua_setfield(L, -2, "is_dir"); + + // Return table + return 1; +} + +static int script_split_path(lua_State *L) +{ + const char *p = luaL_checkstring(L, 1); + bstr fname = mp_dirname(p); + lua_pushlstring(L, fname.start, fname.len); + lua_pushstring(L, mp_basename(p)); + return 2; +} + +static int script_join_path(lua_State *L, void *tmp) +{ + const char *p1 = luaL_checkstring(L, 1); + const char *p2 = luaL_checkstring(L, 2); + char *r = mp_path_join(tmp, p1, p2); + lua_pushstring(L, r); + return 1; +} + +static int script_parse_json(lua_State *L, void *tmp) +{ + mp_lua_optarg(L, 2); + char *text = talloc_strdup(tmp, luaL_checkstring(L, 1)); + bool trail = lua_toboolean(L, 2); + bool ok = false; + struct mpv_node node; + if (json_parse(tmp, &node, &text, MAX_JSON_DEPTH) >= 0) { + json_skip_whitespace(&text); + ok = !text[0] || trail; + } + if (ok) { + pushnode(L, &node); + lua_pushnil(L); + } else { + lua_pushnil(L); + lua_pushstring(L, "error"); + } + lua_pushstring(L, text); + return 3; +} + +static int script_format_json(lua_State *L, void *tmp) +{ + struct mpv_node node; + makenode(tmp, &node, L, 1); + char *dst = talloc_strdup(tmp, ""); + if (json_write(&dst, &node) >= 0) { + lua_pushstring(L, dst); + lua_pushnil(L); + } else { + lua_pushnil(L); + lua_pushstring(L, "error"); + } + return 2; +} + +static int script_get_env_list(lua_State *L) +{ + lua_newtable(L); // table + for (int n = 0; environ && environ[n]; n++) { + lua_pushstring(L, environ[n]); // table str + lua_rawseti(L, -2, n + 1); // table + } + return 1; +} + +#define FN_ENTRY(name) {#name, script_ ## name, 0} +#define AF_ENTRY(name) {#name, 0, script_ ## name} +struct fn_entry { + const char *name; + int (*fn)(lua_State *L); // lua_CFunction + int (*af)(lua_State *L, void *); // af_CFunction +}; + +static const struct fn_entry main_fns[] = { + FN_ENTRY(log), + AF_ENTRY(raw_wait_event), + FN_ENTRY(request_event), + FN_ENTRY(find_config_file), + FN_ENTRY(get_script_directory), + FN_ENTRY(command), + FN_ENTRY(commandv), + AF_ENTRY(command_native), + AF_ENTRY(raw_command_native_async), + FN_ENTRY(raw_abort_async_command), + AF_ENTRY(get_property), + AF_ENTRY(get_property_osd), + FN_ENTRY(get_property_bool), + FN_ENTRY(get_property_number), + AF_ENTRY(get_property_native), + FN_ENTRY(del_property), + FN_ENTRY(set_property), + FN_ENTRY(set_property_bool), + FN_ENTRY(set_property_number), + AF_ENTRY(set_property_native), + FN_ENTRY(raw_observe_property), + FN_ENTRY(raw_unobserve_property), + FN_ENTRY(get_time), + FN_ENTRY(input_set_section_mouse_area), + FN_ENTRY(format_time), + FN_ENTRY(enable_messages), + FN_ENTRY(get_wakeup_pipe), + FN_ENTRY(raw_hook_add), + FN_ENTRY(raw_hook_continue), + {0} +}; + +static const struct fn_entry utils_fns[] = { + AF_ENTRY(readdir), + FN_ENTRY(file_info), + FN_ENTRY(split_path), + AF_ENTRY(join_path), + AF_ENTRY(parse_json), + AF_ENTRY(format_json), + FN_ENTRY(get_env_list), + {0} +}; + +typedef struct autofree_data { + af_CFunction target; + void *ctx; +} autofree_data; + +/* runs the target autofree script_* function with the ctx argument */ +static int script_autofree_call(lua_State *L) +{ + // n*args &data + autofree_data *data = lua_touserdata(L, -1); + lua_pop(L, 1); // n*args + assert(data && data->target && data->ctx); + return data->target(L, data->ctx); +} + +static int script_autofree_trampoline(lua_State *L) +{ + // n*args + autofree_data data = { + .target = lua_touserdata(L, lua_upvalueindex(2)), // fn + .ctx = NULL, + }; + assert(data.target); + + lua_pushvalue(L, lua_upvalueindex(1)); // n*args autofree_call (closure) + lua_insert(L, 1); // autofree_call n*args + lua_pushlightuserdata(L, &data); // autofree_call n*args &data + + data.ctx = talloc_new(NULL); + int r = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0); // m*retvals + talloc_free(data.ctx); + + if (r) + lua_error(L); + + return lua_gettop(L); // m (retvals) +} + +static void af_pushcclosure(lua_State *L, af_CFunction fn, int n) +{ + // Instead of pushing a direct closure of fn with n upvalues, we push an + // autofree_trampoline closure with two upvalues: + // 1: autofree_call closure with the n upvalues given here. + // 2: fn + // + // when called the autofree_trampoline closure will pcall the autofree_call + // closure with the current lua call arguments and an additional argument + // which holds ctx and fn. the autofree_call closure (with the n upvalues + // given here) calls fn directly and provides it with the ctx C argument, + // so that fn sees the exact n upvalues and lua call arguments as intended, + // wrapped with ctx init/cleanup. + + lua_pushcclosure(L, script_autofree_call, n); + lua_pushlightuserdata(L, fn); + lua_pushcclosure(L, script_autofree_trampoline, 2); +} + +static void register_package_fns(lua_State *L, char *module, + const struct fn_entry *e) +{ + push_module_table(L, module); // modtable + for (int n = 0; e[n].name; n++) { + if (e[n].af) { + af_pushcclosure(L, e[n].af, 0); // modtable fn + } else { + lua_pushcclosure(L, e[n].fn, 0); // modtable fn + } + lua_setfield(L, -2, e[n].name); // modtable + } + lua_pop(L, 1); // - +} + +static void add_functions(struct script_ctx *ctx) +{ + lua_State *L = ctx->state; + + register_package_fns(L, "mp", main_fns); + register_package_fns(L, "mp.utils", utils_fns); +} + +const struct mp_scripting mp_scripting_lua = { + .name = "lua", + .file_ext = "lua", + .load = load_lua, +}; |