/* * Copyright © 2025 Red Hat, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "config.h" #include #include #include #include #include #include "util-mem.h" #include "util-strings.h" #include "evdev-frame.h" #include "libinput-feature.h" #include "libinput-log.h" #include "libinput-plugin-lua.h" #include "libinput-plugin.h" #include "libinput-util.h" #include "timer.h" const uint32_t LIBINPUT_PLUGIN_VERSION = 1U; #define PLUGIN_METATABLE "LibinputPlugin" #define EVDEV_DEVICE_METATABLE "EvdevDevice" static const char libinput_lua_plugin_key = 'p'; /* key to lua registry */ static const char libinput_key = 'l'; /* key to lua registry */ DEFINE_TRIVIAL_CLEANUP_FUNC(lua_State *, lua_close); struct udev_property { struct list link; char *key; char *value; }; static inline struct udev_property * udev_property_new(const char *key, const char *value) { struct udev_property *prop = zalloc(sizeof(*prop)); prop->key = safe_strdup(key); prop->value = safe_strdup(value); return prop; } static inline void udev_property_destroy(struct udev_property *prop) { list_remove(&prop->link); free(prop->key); free(prop->value); free(prop); } /* A thin wrapper struct that just needs to exist, all * the actual logic is struct libinput_lua_plugin */ typedef struct { } LibinputPlugin; typedef struct { struct list link; int refid; struct libinput_device *device; unsigned int id; unsigned int bustype; unsigned int vid; unsigned int pid; char *name; struct list udev_properties_list; struct libevdev *evdev; int device_removed_refid; int frame_refid; /* Caches any disable_feature calls during device_new */ bool was_added; bitmask_t disabled_features; } EvdevDevice; struct libinput_lua_plugin { struct libinput_plugin *parent; lua_State *L; int sandbox_table_idx; bool register_called; struct list evdev_devices; /* EvdevDevice */ size_t version; int device_new_refid; int timer_expired_refid; struct libinput_plugin_timer *timer; bool in_timer_func; struct list timer_injected_events; }; static struct libinput_lua_plugin * lua_get_libinput_lua_plugin(lua_State *L) { struct libinput_lua_plugin *plugin = NULL; lua_pushlightuserdata(L, (void *)&libinput_lua_plugin_key); lua_gettable(L, LUA_REGISTRYINDEX); plugin = lua_touserdata(L, -1); lua_pop(L, 1); return plugin; } /* Prints the current stack "layout" with a message */ #define lua_show_stack(L, ...) { \ etrace(__VA_ARGS__); \ etrace("pcall: stack has: %d", lua_gettop(L)); \ for (int i = -1; i >= -lua_gettop(L); i--) \ etrace(" stack %d: %s", i, lua_typename(L, lua_type(L, i))); \ } static struct libinput * lua_get_libinput(lua_State *L) { struct libinput *libinput = NULL; lua_pushlightuserdata(L, (void *)&libinput_key); lua_gettable(L, LUA_REGISTRYINDEX); libinput = lua_touserdata(L, -1); lua_pop(L, 1); return libinput; } static void lua_push_evdev_device(lua_State *L, struct libinput_lua_plugin *plugin, struct libinput_device *device, struct libevdev *evdev, struct udev_device *udev_device) { EvdevDevice *lua_device = lua_newuserdata(L, sizeof(*lua_device)); memset(lua_device, 0, sizeof(*lua_device)); lua_device->device = libinput_device_ref(device); lua_device->evdev = evdev; lua_device->bustype = libinput_device_get_id_bustype(device); lua_device->vid = libinput_device_get_id_vendor(device); lua_device->pid = libinput_device_get_id_product(device); lua_device->name = strdup(libinput_device_get_name(device)); lua_device->device_removed_refid = LUA_NOREF; lua_device->frame_refid = LUA_NOREF; list_init(&lua_device->udev_properties_list); struct udev_list_entry *e = udev_device_get_properties_list_entry(udev_device); while (e) { const char *key = udev_list_entry_get_name(e); if (strstartswith(key, "ID_INPUT_") && !streq(key, "ID_INPUT_WIDTH_MM") && !streq(key, "ID_INPUT_HEIGHT_MM")) { const char *value = udev_list_entry_get_value(e); if (!streq(value, "0")) { struct udev_property *prop = udev_property_new(key, value); list_insert(&lua_device->udev_properties_list, &prop->link); } } e = udev_list_entry_get_next(e); } list_insert(&plugin->evdev_devices, &lua_device->link); lua_pushvalue(L, -1); /* Copy to top */ lua_device->refid = luaL_ref(L, LUA_REGISTRYINDEX); /* ref to device */ luaL_getmetatable(L, EVDEV_DEVICE_METATABLE); lua_setmetatable(L, -2); } static void lua_push_evdev_frame(lua_State *L, struct evdev_frame *frame) { size_t nevents; struct evdev_event *events = evdev_frame_get_events(frame, &nevents); lua_newtable(L); for (size_t i = 0; i < nevents; i++) { struct evdev_event *e = &events[i]; if (evdev_usage_eq(e->usage, EVDEV_SYN_REPORT)) break; lua_newtable(L); lua_pushinteger(L, evdev_usage_as_uint32_t(e->usage)); lua_setfield(L, -2, "usage"); lua_pushinteger(L, e->value); lua_setfield(L, -2, "value"); lua_rawseti(L, -2, i + 1); } } static void lua_pop_evdev_frame(struct libinput_lua_plugin *plugin, struct evdev_frame *frame_out) { lua_State *L = plugin->L; if (lua_isnil(L, -1)) { lua_pop(L, 1); return; } if (!lua_istable(L, -1)) { plugin_log_bug(plugin->parent, "expected table like `{ events = { ... } }`, got %s", lua_typename(L, lua_type(L, -1))); return; } struct evdev_event events[64] = { 0 }; size_t nevents = 0; lua_pushnil(L); while (lua_next(L, -2) != 0 && nevents < ARRAY_LENGTH(events)) { /* -2 is the index, -1 our { usage = ... } table */ if (!lua_istable(L, -1)) { plugin_log_bug( plugin->parent, "expected table like `{ type = ..., code = ...}`, got %s", lua_typename(L, lua_type(L, -1))); lua_pop(L, 1); return; } lua_getfield(L, -1, "usage"); uint32_t usage = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_getfield(L, -1, "value"); int32_t value = luaL_checkinteger(L, -1); lua_pop(L, 1); lua_pop(L, 1); /* pop { usage = ..., value = ...} */ struct evdev_event *e = &events[nevents++]; e->usage = evdev_usage_from_uint32_t(usage); e->value = value; if (evdev_usage_eq(e->usage, EVDEV_SYN_REPORT)) { lua_pop(L, 1); /* force-pop the nil */ break; } } if (nevents == 0) { events[0].usage = evdev_usage_from_uint32_t(EVDEV_SYN_REPORT); events[0].value = 0; nevents++; } if (evdev_frame_set(frame_out, events, nevents) == -ENOMEM) { plugin_log_bug(plugin->parent, "too many events in frame"); } } static bool libinput_lua_pcall(struct libinput_lua_plugin *plugin, int narg, int nres) { lua_State *L = plugin->L; lua_pushvalue(L, -(narg + 1)); /* Copy the function */ lua_pushvalue(L, plugin->sandbox_table_idx); /* Now set the sandbox environment for the function we're about to call */ int rc = lua_setfenv(L, -2) == 1 ? LUA_OK : LUA_ERRRUN; if (rc == LUA_OK) { /* Replace original function with sandboxed one */ lua_replace(L, -(narg + 2)); /* Now call the function */ rc = lua_pcall(L, narg, nres, 0); } else { lua_pushstring(L, "Failed to set up sandbox"); } if (rc != LUA_OK) { auto libinput_plugin = plugin->parent; const char *errormsg = lua_tostring(L, -1); if (strstr(errormsg, "@@unregistering@@") == NULL) { plugin_log_bug(libinput_plugin, "unloading after error: %s\n", errormsg); } lua_pop(L, 1); /* pop error message */ if (plugin->timer) libinput_plugin_timer_cancel(plugin->timer); libinput_plugin_unregister(libinput_plugin); /* plugin system will destroy the plugin later */ } return rc == LUA_OK; } static void libinput_lua_plugin_device_new(struct libinput_plugin *libinput_plugin, struct libinput_device *device, struct libevdev *evdev, struct udev_device *udev_device) { struct libinput_lua_plugin *plugin = libinput_plugin_get_user_data(libinput_plugin); lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, plugin->device_new_refid); lua_push_evdev_device(plugin->L, plugin, device, evdev, udev_device); libinput_lua_pcall(plugin, 1, 0); } static void remove_device(struct libinput_lua_plugin *plugin, EvdevDevice *evdev) { /* Don't allow access to the libevdev context during remove */ evdev->evdev = NULL; if (evdev->device_removed_refid != LUA_NOREF) { lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->device_removed_refid); lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->refid); if (!libinput_lua_pcall(plugin, 1, 0)) return; } luaL_unref(plugin->L, evdev->refid, LUA_REGISTRYINDEX); evdev->refid = LUA_NOREF; list_remove(&evdev->link); list_init(&evdev->link); /* so we can list_remove in _gc */ struct udev_property *prop; list_for_each_safe(prop, &evdev->udev_properties_list, link) { udev_property_destroy(prop); } free(evdev->name); evdev->name = NULL; evdev->device = libinput_device_unref(evdev->device); /* This device no longer exists but our lua code may have a * reference to it */ } static void libinput_lua_plugin_device_ignored(struct libinput_plugin *libinput_plugin, struct libinput_device *device) { struct libinput_lua_plugin *plugin = libinput_plugin_get_user_data(libinput_plugin); EvdevDevice *evdev; list_for_each_safe(evdev, &plugin->evdev_devices, link) { if (evdev->device != device) continue; remove_device(plugin, evdev); } } static void libinput_lua_plugin_device_removed(struct libinput_plugin *libinput_plugin, struct libinput_device *device) { struct libinput_lua_plugin *plugin = libinput_plugin_get_user_data(libinput_plugin); EvdevDevice *evdev; list_for_each_safe(evdev, &plugin->evdev_devices, link) { if (evdev->device != device) continue; remove_device(plugin, evdev); } } static void libinput_lua_plugin_evdev_frame(struct libinput_plugin *libinput_plugin, struct libinput_device *device, struct evdev_frame *frame) { struct libinput_lua_plugin *plugin = libinput_plugin_get_user_data(libinput_plugin); EvdevDevice *evdev; list_for_each_safe(evdev, &plugin->evdev_devices, link) { if (evdev->device != device) continue; if (evdev->frame_refid == LUA_NOREF) continue; lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->frame_refid); lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->refid); lua_push_evdev_frame(plugin->L, frame); lua_pushinteger(plugin->L, evdev_frame_get_time(frame)); if (!libinput_lua_pcall(plugin, 3, 1)) return; lua_pop_evdev_frame(plugin, frame); } } static void register_func(struct lua_State *L, int stack_index, int *refid) { if (*refid != LUA_NOREF) luaL_unref(L, LUA_REGISTRYINDEX, *refid); lua_pushvalue(L, stack_index); /* Copy function to top */ *refid = luaL_ref(L, LUA_REGISTRYINDEX); /* ref to function */ } static void unregister_func(struct lua_State *L, int *refid) { if (*refid != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, *refid); *refid = LUA_NOREF; } } static int libinputplugin_connect(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); const char *name = luaL_checkstring(L, 2); luaL_checktype(L, 3, LUA_TFUNCTION); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); /* Version 1 signals */ if (streq(name, "new-evdev-device")) { register_func(L, 3, &plugin->device_new_refid); } else if (streq(name, "timer-expired")) { register_func(L, 3, &plugin->timer_expired_refid); } else { return luaL_error(L, "Unknown name: %s", name); } return 0; } static int libinputplugin_now(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); struct libinput *libinput = lua_get_libinput(L); uint64_t now = libinput_now(libinput); lua_pushinteger(L, now); return 1; } static int libinputplugin_version(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); lua_pushinteger(L, plugin->version); return 1; } static int libinputplugin_register(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); if (plugin->register_called) { return luaL_error(L, "plugin already registered"); } uint32_t versions[16] = { 0 }; size_t idx = 0; luaL_checktype(L, 2, LUA_TTABLE); lua_pushnil(L); while (idx < ARRAY_LENGTH(versions) && lua_next(L, -2) != 0) { int version = luaL_checkinteger(L, -1); lua_pop(L, 1); if (version <= 0) { return luaL_error(L, "Invalid version number"); } versions[idx++] = version; } ARRAY_FOR_EACH(versions, v) { if (*v == 0) break; if (*v == LIBINPUT_PLUGIN_VERSION) { plugin->version = *v; plugin->register_called = true; lua_pushinteger(L, plugin->version); return 1; } } return luaL_error(L, "None of this plugin's versions are supported"); } static int libinputplugin_unregister(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); /* Bit of a hack: unregister should work like os.exit(1) * but we're in a lua context here so the easiest way * to handle this is pretend we have an error, let * our error handler unwind and just search for this * magic string to *not* print log message */ return luaL_error(L, "@@unregistering@@"); } static int libinputplugin_gc(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); if (plugin->timer) libinput_plugin_timer_cancel(plugin->timer); /* We're about to destroy the plugin so the timer is the only * thing we need to stop, the rest will be cleaned up * when we destroy the plugin */ return 0; } struct timer_injected_event { struct list link; struct evdev_frame *frame; int device_refid; }; static void plugin_timer_func(struct libinput_plugin *libinput_plugin, uint64_t now, void *data) { struct libinput_lua_plugin *plugin = data; struct lua_State *L = plugin->L; lua_rawgeti(L, LUA_REGISTRYINDEX, plugin->timer_expired_refid); lua_pushinteger(L, now); /* To allow for injecting events */ plugin->in_timer_func = true; libinput_lua_pcall(plugin, 1, 0); plugin->in_timer_func = false; struct timer_injected_event *injected_event; list_for_each_safe(injected_event, &plugin->timer_injected_events, link) { EvdevDevice *device; list_for_each(device, &plugin->evdev_devices, link) { if (device->refid == injected_event->device_refid) { libinput_plugin_inject_evdev_frame( plugin->parent, device->device, injected_event->frame); break; } } list_remove(&injected_event->link); evdev_frame_unref(injected_event->frame); free(injected_event); } } static int libinputplugin_timer_set(lua_State *L, uint64_t offset) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); uint64_t timeout = luaL_checkinteger(L, 2); if (!plugin->timer) { plugin->timer = libinput_plugin_timer_new( plugin->parent, libinput_plugin_get_name(plugin->parent), plugin_timer_func, plugin); } libinput_plugin_timer_set(plugin->timer, offset + timeout); return 0; } static int libinputplugin_timer_set_absolute(lua_State *L) { return libinputplugin_timer_set(L, 0); } static int libinputplugin_timer_set_relative(lua_State *L) { auto libinput = lua_get_libinput(L); return libinputplugin_timer_set(L, libinput_now(libinput)); } static int libinputplugin_timer_cancel(lua_State *L) { LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); if (plugin->timer) libinput_plugin_timer_cancel(plugin->timer); return 0; } static const struct luaL_Reg libinputplugin_vtable[] = { { "now", libinputplugin_now }, { "version", libinputplugin_version }, { "connect", libinputplugin_connect }, { "register", libinputplugin_register }, { "unregister", libinputplugin_unregister }, { "timer_cancel", libinputplugin_timer_cancel }, { "timer_set_absolute", libinputplugin_timer_set_absolute }, { "timer_set_relative", libinputplugin_timer_set_relative }, { "__gc", libinputplugin_gc }, { NULL, NULL } }; static void libinputplugin_init(lua_State *L) { luaL_newmetatable(L, PLUGIN_METATABLE); lua_pushstring(L, "__index"); lua_pushvalue(L, -2); /* push metatable */ lua_settable(L, -3); /* metatable.__index = metatable */ luaL_setfuncs(L, libinputplugin_vtable, 0); } static int evdevdevice_info(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); lua_newtable(L); /* { bustype: ..., vid: ..., pid: ..., name: ... } */ lua_pushinteger(L, device->bustype); lua_setfield(L, -2, "bustype"); lua_pushinteger(L, device->vid); lua_setfield(L, -2, "vid"); lua_pushinteger(L, device->pid); lua_setfield(L, -2, "pid"); return 1; } static int evdevdevice_name(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); lua_pushstring(L, device->name); return 1; } static int evdevdevice_usages(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); lua_newtable(L); /* { evdev.REL_X: ... } */ if (device->evdev == NULL) return 1; for (unsigned int t = 0; t < EV_MAX; t++) { if (!libevdev_has_event_type(device->evdev, t)) continue; int max = libevdev_event_type_get_max(t); for (unsigned int code = 0; (int)code < max; code++) { if (!libevdev_has_event_code(device->evdev, t, code)) continue; evdev_usage_t usage = evdev_usage_from_code(t, code); lua_pushboolean(L, true); lua_rawseti(L, -2, evdev_usage_as_uint32_t(usage)); } } return 1; } static int evdevdevice_absinfos(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); lua_newtable(L); /* { ABS_X: { min: 1, max: 2, ... }, ... } */ if (device->evdev == NULL) return 1; for (unsigned int code = 0; code < ABS_MAX; code++) { const struct input_absinfo *abs = libevdev_get_abs_info(device->evdev, code); if (!abs) continue; lua_newtable(L); lua_pushinteger(L, abs->minimum); lua_setfield(L, -2, "minimum"); lua_pushinteger(L, abs->maximum); lua_setfield(L, -2, "maximum"); lua_pushinteger(L, abs->fuzz); lua_setfield(L, -2, "fuzz"); lua_pushinteger(L, abs->flat); lua_setfield(L, -2, "flat"); lua_pushinteger(L, abs->resolution); lua_setfield(L, -2, "resolution"); evdev_usage_t usage = evdev_usage_from_code(EV_ABS, code); lua_rawseti( L, -2, evdev_usage_as_uint32_t(usage)); /* Assign to top-level table */ } return 1; } static int evdevdevice_udev_properties(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); lua_newtable(L); /* { ID_INPUT: { ... } , ... } */ if (device->evdev == NULL) return 1; struct udev_property *prop; list_for_each(prop, &device->udev_properties_list, link) { lua_pushstring(L, prop->value); lua_setfield(L, -2, prop->key); /* Assign to top-level table */ } return 1; } static int evdevdevice_enable_evdev_usage(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); evdev_usage_t usage = evdev_usage_from_uint32_t(luaL_checkinteger(L, 2)); uint16_t type = evdev_usage_type(usage); uint16_t code = evdev_usage_code(usage); if (type > EV_MAX) { plugin_log_bug(plugin->parent, , "Ignoring invalid evdev usage %#x\n", evdev_usage_as_uint32_t(usage)); return 0; } if (device->evdev == NULL || type == EV_ABS) return 0; libevdev_enable_event_code(device->evdev, type, code, NULL); return 0; } static int evdevdevice_disable_evdev_usage(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); evdev_usage_t usage = evdev_usage_from_uint32_t(luaL_checkinteger(L, 2)); uint16_t type = evdev_usage_type(usage); uint16_t code = evdev_usage_code(usage); if (device->evdev == NULL || type > EV_MAX) return 0; libevdev_disable_event_code(device->evdev, type, code); return 0; } static int evdevdevice_set_absinfo(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); evdev_usage_t usage = evdev_usage_from_uint32_t(luaL_checkinteger(L, 2)); luaL_checktype(L, 3, LUA_TTABLE); if (evdev_usage_type(usage) != EV_ABS) return 0; if (!device->evdev) return 0; uint16_t code = evdev_usage_code(usage); const struct input_absinfo *absinfo = libevdev_get_abs_info(device->evdev, code); struct input_absinfo abs = {}; if (absinfo) abs = *absinfo; lua_getfield(L, 3, "minimum"); if (lua_isnumber(L, -1)) abs.minimum = luaL_checkinteger(L, -1); lua_getfield(L, 3, "maximum"); if (lua_isnumber(L, -1)) abs.maximum = luaL_checkinteger(L, -1); lua_getfield(L, 3, "resolution"); if (lua_isnumber(L, -1)) abs.resolution = luaL_checkinteger(L, -1); lua_getfield(L, 3, "fuzz"); if (lua_isnumber(L, -1)) abs.fuzz = luaL_checkinteger(L, -1); lua_getfield(L, 3, "flat"); if (lua_isnumber(L, -1)) abs.flat = luaL_checkinteger(L, -1); libevdev_enable_event_code(device->evdev, EV_ABS, code, &abs); return 0; } static int evdevdevice_connect(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); const char *name = luaL_checkstring(L, 2); luaL_checktype(L, 3, LUA_TFUNCTION); /* No refid means we got removed, so quietly * drop any connect call */ if (device->refid == LUA_NOREF) return 0; if (streq(name, "device-removed")) { register_func(L, 3, &device->device_removed_refid); } else if (streq(name, "evdev-frame")) { struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); libinput_plugin_enable_device_event_frame(plugin->parent, device->device, true); register_func(L, 3, &device->frame_refid); } else { return luaL_error(L, "Unknown name: %s", name); } return 0; } static int evdevdevice_disconnect(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); const char *name = luaL_checkstring(L, 2); /* No refid means we got removed, so quietly * drop any disconnect call */ if (device->refid == LUA_NOREF) return 0; if (streq(name, "device-removed")) { unregister_func(L, &device->device_removed_refid); } else if (streq(name, "evdev-frame")) { struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); libinput_plugin_enable_device_event_frame(plugin->parent, device->device, false); unregister_func(L, &device->frame_refid); } else { return luaL_error(L, "Unknown name: %s", name); } return 0; } static struct evdev_frame * evdevdevice_frame(lua_State *L, struct libinput_lua_plugin *plugin) { auto frame = evdev_frame_new(64); lua_pop_evdev_frame(plugin, frame); struct libinput *libinput = lua_get_libinput(L); uint64_t now = libinput_now(libinput); evdev_frame_set_time(frame, now); return frame; } static int evdevdevice_inject_frame(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); luaL_checktype(L, 2, LUA_TTABLE); /* No refid means we got removed, so quietly * drop any disconnect call */ if (device->refid == LUA_NOREF) return 0; struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); if (!plugin->in_timer_func) { return luaL_error(L, "Injecting events only possible in a timer func"); } _unref_(evdev_frame) *frame = evdevdevice_frame(L, plugin); /* Lua is unhappy if we inject an event which calls into our lua state * immediately so we need to queue this for later when we're out of the timer * func */ struct timer_injected_event *event = zalloc(sizeof(*event)); event->device_refid = device->refid; event->frame = steal(&frame); list_insert(&plugin->timer_injected_events, &event->link); return 0; } static int evdevdevice_prepend_frame(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); luaL_checktype(L, 2, LUA_TTABLE); /* No refid means we got removed, so quietly * drop any disconnect call */ if (device->refid == LUA_NOREF) return 0; struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); _unref_(evdev_frame) *frame = evdevdevice_frame(L, plugin); /* FIXME: need to really ensure that the device can never be dangling */ libinput_plugin_prepend_evdev_frame(plugin->parent, device->device, frame); return 0; } static int evdevdevice_append_frame(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); luaL_checktype(L, 2, LUA_TTABLE); /* No refid means we got removed, so quietly * drop any disconnect call */ if (device->refid == LUA_NOREF) return 0; struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); _unref_(evdev_frame) *frame = evdevdevice_frame(L, plugin); /* FIXME: need to really ensure that the device can never be dangling */ libinput_plugin_append_evdev_frame(plugin->parent, device->device, frame); return 0; } static int evdevdevice_disable_feature(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); const char *feature = luaL_checkstring(L, 2); /* No refid means we got removed, so quietly * drop any call */ if (device->refid == LUA_NOREF) return 0; const struct { const char *name; enum libinput_feature feature; } map[] = { { "button-debouncing", LIBINPUT_FEATURE_BUTTON_DEBOUNCING }, { "wheel-debouncing", LIBINPUT_FEATURE_WHEEL_DEBOUNCING }, { "touchpad-jump-detection", LIBINPUT_FEATURE_TOUCHPAD_JUMP_DETECTION }, { "touchpad-palm-detection", LIBINPUT_FEATURE_TOUCHPAD_PALM_DETECTION }, { "touchpad-hysteresis", LIBINPUT_FEATURE_TOUCHPAD_HYSTERESIS }, }; ARRAY_FOR_EACH(map, m) { if (streq(feature, m->name)) { struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); libinput_plugin_disable_device_feature(plugin->parent, device->device, m->feature); return 0; } } return luaL_error(L, "Unknown feature: %s", feature); } static int evdevdevice_gc(lua_State *L) { EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); list_remove(&device->link); struct udev_property *prop; list_for_each_safe(prop, &device->udev_properties_list, link) { udev_property_destroy(prop); } free(device->name); return 0; } static const struct luaL_Reg evdevdevice_vtable[] = { { "info", evdevdevice_info }, { "name", evdevdevice_name }, { "usages", evdevdevice_usages }, { "absinfos", evdevdevice_absinfos }, { "udev_properties", evdevdevice_udev_properties }, { "enable_evdev_usage", evdevdevice_enable_evdev_usage }, { "disable_evdev_usage", evdevdevice_disable_evdev_usage }, { "set_absinfo", evdevdevice_set_absinfo }, { "connect", evdevdevice_connect }, { "disconnect", evdevdevice_disconnect }, { "inject_frame", evdevdevice_inject_frame }, { "prepend_frame", evdevdevice_prepend_frame }, { "append_frame", evdevdevice_append_frame }, { "disable_feature", evdevdevice_disable_feature }, { "__gc", evdevdevice_gc }, { NULL, NULL } }; static void evdevdevice_init(lua_State *L) { luaL_newmetatable(L, EVDEV_DEVICE_METATABLE); lua_pushstring(L, "__index"); lua_pushvalue(L, -2); /* push metatable */ lua_settable(L, -3); /* metatable.__index = metatable */ luaL_setfuncs(L, evdevdevice_vtable, 0); } static int logfunc(lua_State *L, enum libinput_log_priority pri) { auto plugin = lua_get_libinput_lua_plugin(L); const char *message = luaL_checkstring(L, 1); plugin_log_msg(plugin->parent, pri, "%s\n", message); return 0; } static int log_lua_error(lua_State *L) { return logfunc(L, LIBINPUT_LOG_PRIORITY_ERROR); } static int log_lua_info(lua_State *L) { return logfunc(L, LIBINPUT_LOG_PRIORITY_INFO); } static int log_lua_debug(lua_State *L) { return logfunc(L, LIBINPUT_LOG_PRIORITY_DEBUG); } /* Exposes log.debug, log.info, log.error() */ static const struct luaL_Reg log_funcs[] = { { "debug", log_lua_debug }, { "info", log_lua_info }, { "error", log_lua_error }, { NULL, NULL } }; static void libinput_lua_plugin_destroy(struct libinput_lua_plugin *plugin) { if (plugin->timer) libinput_plugin_timer_cancel(plugin->timer); EvdevDevice *evdev; list_for_each_safe(evdev, &plugin->evdev_devices, link) { remove_device(plugin, evdev); } if (plugin->timer) plugin->timer = libinput_plugin_timer_unref(plugin->timer); if (plugin->L) lua_close(plugin->L); free(plugin); } DEFINE_DESTROY_CLEANUP_FUNC(libinput_lua_plugin); static void libinput_plugin_destroy(struct libinput_plugin *libinput_plugin) { struct libinput_lua_plugin *plugin = libinput_plugin_get_user_data(libinput_plugin); if (plugin) libinput_lua_plugin_destroy(plugin); } static void libinput_lua_plugin_run(struct libinput_plugin *libinput_plugin) { struct libinput_lua_plugin *plugin = libinput_plugin_get_user_data(libinput_plugin); if (libinput_lua_pcall(plugin, 0, 0) && !plugin->register_called) { plugin_log_bug(libinput_plugin, "plugin never registered, unloading plugin\n"); libinput_plugin_unregister(libinput_plugin); /* plugin system will destroy the plugin later */ } } static void libinput_lua_init_evdev_global(lua_State *L, int sandbox_table_idx) { lua_newtable(L); for (unsigned int t = 0; t < EV_MAX; t++) { const char *typename = libevdev_event_type_get_name(t); if (!typename) continue; int max = libevdev_event_type_get_max(t); if (max < 0) continue; for (int i = 0; i < max; i++) { const char *name = libevdev_event_code_get_name(t, i); if (!name) continue; evdev_usage_t usage = evdev_usage_from_code(t, i); lua_pushinteger(L, evdev_usage_as_uint32_t(usage)); lua_setfield(L, -2, name); } } #define pushbus(name, value) do { \ lua_pushinteger(L, value); \ lua_setfield(L, -2, #name); \ } while (0) pushbus(BUS_PCI, 0x01); pushbus(BUS_ISAPNP, 0x02); pushbus(BUS_USB, 0x03); pushbus(BUS_HIL, 0x04); pushbus(BUS_BLUETOOTH, 0x05); pushbus(BUS_VIRTUAL, 0x06); pushbus(BUS_ISA, 0x10); pushbus(BUS_I8042, 0x11); pushbus(BUS_XTKBD, 0x12); pushbus(BUS_RS232, 0x13); pushbus(BUS_GAMEPORT, 0x14); pushbus(BUS_PARPORT, 0x15); pushbus(BUS_AMIGA, 0x16); pushbus(BUS_ADB, 0x17); pushbus(BUS_I2C, 0x18); pushbus(BUS_HOST, 0x19); pushbus(BUS_GSC, 0x1A); pushbus(BUS_ATARI, 0x1B); pushbus(BUS_SPI, 0x1C); pushbus(BUS_RMI, 0x1D); pushbus(BUS_CEC, 0x1E); pushbus(BUS_INTEL_ISHTP, 0x1F); pushbus(BUS_AMD_SFH, 0x20); #undef pushbus lua_setfield(L, sandbox_table_idx, "evdev"); } static const struct libinput_plugin_interface interface = { .run = libinput_lua_plugin_run, .destroy = libinput_plugin_destroy, .device_new = libinput_lua_plugin_device_new, .device_ignored = libinput_lua_plugin_device_ignored, .device_added = NULL, .device_removed = libinput_lua_plugin_device_removed, .evdev_frame = libinput_lua_plugin_evdev_frame, }; static lua_State * libinput_lua_plugin_init_lua(struct libinput *libinput, struct libinput_lua_plugin *plugin) { lua_State *L = luaL_newstate(); if (!L) return NULL; /* This will be our our global env later, see libinput_lua_pcall() */ lua_newtable(L); int sandbox_table_idx = lua_gettop(L); plugin->sandbox_table_idx = sandbox_table_idx; /* Load the modules we want to (partially) expose. * An (outdated?) list of safe function is here: * http://lua-users.org/wiki/SandBoxes * * Math, String and Table seem to be safe given that our plugins * all have their own individual sandbox. */ luaopen_base(L); static const char *allowed_funcs[] = { "assert", "error", "ipairs", "next", "pcall", "pairs", "print", "tonumber", "tostring", "type", "unpack", "xpcall", }; ARRAY_FOR_EACH(allowed_funcs, func) { lua_getglobal(L, *func); lua_setfield(L, sandbox_table_idx, *func); } /* Math is fine as a whole */ luaopen_math(L); lua_getglobal(L, "math"); lua_setfield(L, sandbox_table_idx, "math"); /* Table is fine as a whole */ luaopen_table(L); lua_getglobal(L, "table"); lua_setfield(L, sandbox_table_idx, "table"); /* String is fine as a whole */ luaopen_string(L); lua_getglobal(L, "string"); lua_setfield(L, sandbox_table_idx, "string"); /* Override metatable to prevent access to unregistered globals */ lua_newtable(L); lua_pushstring(L, "__index"); lua_pushnil(L); lua_settable(L, -3); lua_setmetatable(L, sandbox_table_idx); /* Our objects */ libinputplugin_init(L); evdevdevice_init(L); /* Our globals */ lua_newtable(L); luaL_register(L, "log", log_funcs); lua_setfield(L, sandbox_table_idx, "log"); libinput_lua_init_evdev_global(L, sandbox_table_idx); /* The libinput global object */ lua_newuserdata(L, sizeof(LibinputPlugin)); luaL_getmetatable(L, PLUGIN_METATABLE); lua_setmetatable(L, -2); lua_setfield(L, sandbox_table_idx, "libinput"); /* Make struct libinput available in our callbacks */ lua_pushlightuserdata(L, (void *)&libinput_key); lua_pushlightuserdata(L, libinput); lua_settable(L, LUA_REGISTRYINDEX); /* Make struct libinput_lua_plugin available in our callbacks */ lua_pushlightuserdata(L, (void *)&libinput_lua_plugin_key); lua_pushlightuserdata(L, plugin); lua_settable(L, LUA_REGISTRYINDEX); return L; } struct libinput_plugin * libinput_lua_plugin_new_from_path(struct libinput *libinput, const char *path) { _destroy_(libinput_lua_plugin) *plugin = zalloc(sizeof(*plugin)); _autofree_ char *name = safe_strdup(safe_basename(path)); /* libinput's plugin system keeps a ref, we don't need * a separate ref here, the plugin system will outlast us. */ _unref_(libinput_plugin) *p = libinput_plugin_new(libinput, name, &interface, NULL); plugin->parent = p; plugin->register_called = false; plugin->version = LIBINPUT_PLUGIN_VERSION; plugin->device_new_refid = LUA_NOREF; plugin->timer_expired_refid = LUA_NOREF; list_init(&plugin->evdev_devices); list_init(&plugin->timer_injected_events); _cleanup_(lua_closep) lua_State *L = libinput_lua_plugin_init_lua(libinput, plugin); if (!L) { plugin_log_bug(plugin->parent, "Failed to create lua state for %s\n", name); libinput_plugin_unregister(p); return NULL; } int ret = luaL_loadfile(L, path); if (ret == LUA_OK) { plugin->L = steal(&L); libinput_plugin_set_user_data(p, steal(&plugin)); return p; } else { const char *lua_error = lua_tostring(L, -1); const char *error = lua_error; if (!error) { switch (ret) { case LUA_ERRMEM: error = "out of memory"; break; case LUA_ERRFILE: error = "file not found or not readable"; break; case LUA_ERRSYNTAX: error = "syntax error"; break; default: break; } } if (ret == LUA_ERRSYNTAX && log_is_logged(libinput, LIBINPUT_LOG_PRIORITY_DEBUG)) { luaL_traceback(L, L, NULL, 1); for (int i = -1; i > -4; i--) { const char *msg = lua_tostring(L, i); if (!msg) break; log_debug(libinput, "%s %s\n", name, msg); } lua_pop(L, 1); /* traceback */ } plugin_log_bug(plugin->parent, "Failed to load %s: %s\n", path, error); lua_pop(L, 1); /* the lua_error message */ libinput_plugin_unregister(p); return NULL; } }