Add plugin permissions and IO API (#5231)
This commit is contained in:
@@ -74,11 +74,13 @@ ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Called c2.Channel method with a non Channel 'self' argument.");
|
||||
L, "Called c2.Channel method with a non-userdata 'self' argument");
|
||||
return nullptr;
|
||||
}
|
||||
auto *data = WeakPtrUserData<UserData::Type::Channel, Channel>::from(
|
||||
lua_touserdata(L, lua_gettop(L)));
|
||||
// luaL_checkudata is no-return if check fails
|
||||
auto *checked = luaL_checkudata(L, lua_gettop(L), "c2.Channel");
|
||||
auto *data =
|
||||
WeakPtrUserData<UserData::Type::Channel, Channel>::from(checked);
|
||||
if (data == nullptr)
|
||||
{
|
||||
luaL_error(L,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
#include <optional>
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
# include <optional>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
|
||||
# include "Application.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
# include "lauxlib.h"
|
||||
# include "lua.h"
|
||||
|
||||
# include <cerrno>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
// Note: Parsing and then serializing the mode ensures we understand it before
|
||||
// passing it to Lua
|
||||
|
||||
struct LuaFileMode {
|
||||
char major = 'r'; // 'r'|'w'|'a'
|
||||
bool update{}; // '+'
|
||||
bool binary{}; // 'b'
|
||||
QString error;
|
||||
|
||||
LuaFileMode() = default;
|
||||
|
||||
LuaFileMode(const QString &smode)
|
||||
{
|
||||
if (smode.isEmpty())
|
||||
{
|
||||
this->error = "Empty mode given, use one matching /[rwa][+]?b?/.";
|
||||
return;
|
||||
}
|
||||
auto major = smode.at(0);
|
||||
if (major != 'r' && major != 'w' && major != 'a')
|
||||
{
|
||||
this->error = "Invalid mode, use one matching /[rwa][+]?b?/. "
|
||||
"Parsing failed at 1st character.";
|
||||
return;
|
||||
}
|
||||
this->major = major.toLatin1();
|
||||
if (smode.length() > 1)
|
||||
{
|
||||
auto plusOrB = smode.at(1);
|
||||
if (plusOrB == '+')
|
||||
{
|
||||
this->update = true;
|
||||
}
|
||||
else if (plusOrB == 'b')
|
||||
{
|
||||
this->binary = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->error = "Invalid mode, use one matching /[rwa][+]?b?/. "
|
||||
"Parsing failed at 2nd character.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (smode.length() > 2)
|
||||
{
|
||||
auto maybeB = smode.at(2);
|
||||
if (maybeB == 'b')
|
||||
{
|
||||
this->binary = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
this->error = "Invalid mode, use one matching /[rwa][+]?b?/. "
|
||||
"Parsing failed at 3rd character.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString toString() const
|
||||
{
|
||||
assert(this->major == 'r' || this->major == 'w' || this->major == 'a');
|
||||
QString out;
|
||||
out += this->major;
|
||||
if (this->update)
|
||||
{
|
||||
out += '+';
|
||||
}
|
||||
if (this->binary)
|
||||
{
|
||||
out += 'b';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
int ioError(lua_State *L, const QString &value, int errnoequiv)
|
||||
{
|
||||
lua_pushnil(L);
|
||||
lua::push(L, value);
|
||||
lua::push(L, errnoequiv);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(*vararg)
|
||||
int io_open(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
LuaFileMode mode;
|
||||
if (lua_gettop(L) == 2)
|
||||
{
|
||||
// we have a mode
|
||||
QString smode;
|
||||
if (!lua::pop(L, &smode))
|
||||
{
|
||||
return luaL_error(
|
||||
L,
|
||||
"io.open mode (2nd argument) must be a string or not present");
|
||||
}
|
||||
mode = LuaFileMode(smode);
|
||||
if (!mode.error.isEmpty())
|
||||
{
|
||||
return luaL_error(L, mode.error.toStdString().c_str());
|
||||
}
|
||||
}
|
||||
QString filename;
|
||||
if (!lua::pop(L, &filename))
|
||||
{
|
||||
return luaL_error(L,
|
||||
"io.open filename (1st argument) must be a string");
|
||||
}
|
||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||
auto abs = file.absoluteFilePath();
|
||||
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||
<< "] Plugin is opening file at " << abs
|
||||
<< " with mode " << mode.toString();
|
||||
bool ok = pl->hasFSPermissionFor(
|
||||
mode.update || mode.major == 'w' || mode.major == 'a', abs);
|
||||
if (!ok)
|
||||
{
|
||||
return ioError(L,
|
||||
"Plugin does not have permissions to access given file.",
|
||||
EACCES);
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "open");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua::push(L, abs);
|
||||
lua::push(L, mode.toString());
|
||||
lua_call(L, 2, 3);
|
||||
return 3;
|
||||
}
|
||||
|
||||
int io_lines(lua_State *L)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
if (lua_gettop(L) == 0)
|
||||
{
|
||||
// io.lines() case, just call realio.lines
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "lines");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_call(L, 0, 1);
|
||||
return 1;
|
||||
}
|
||||
QString filename;
|
||||
if (!lua::pop(L, &filename))
|
||||
{
|
||||
return luaL_error(
|
||||
L,
|
||||
"io.lines filename (1st argument) must be a string or not present");
|
||||
}
|
||||
QFileInfo file(pl->dataDirectory().filePath(filename));
|
||||
auto abs = file.absoluteFilePath();
|
||||
qCDebug(chatterinoLua) << "[" << pl->id << ":" << pl->meta.name
|
||||
<< "] Plugin is opening file at " << abs
|
||||
<< " for reading lines";
|
||||
bool ok = pl->hasFSPermissionFor(false, abs);
|
||||
if (!ok)
|
||||
{
|
||||
return ioError(L,
|
||||
"Plugin does not have permissions to access given file.",
|
||||
EACCES);
|
||||
}
|
||||
// Our stack looks like this:
|
||||
// - {...}[1]
|
||||
// - {...}[2]
|
||||
// ...
|
||||
// We want:
|
||||
// - REG[REG_REAL_IO_NAME].lines
|
||||
// - absolute file path
|
||||
// - {...}[1]
|
||||
// - {...}[2]
|
||||
// ...
|
||||
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
lua_getfield(L, -1, "lines");
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_insert(L, 1); // move function to start of stack
|
||||
lua::push(L, abs);
|
||||
lua_insert(L, 2); // move file name just after the function
|
||||
lua_call(L, lua_gettop(L) - 1, LUA_MULTRET);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// This is the code for both io.input and io.output
|
||||
int globalFileCommon(lua_State *L, bool output)
|
||||
{
|
||||
auto *pl = getApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
luaL_error(L, "internal error: no plugin");
|
||||
return 0;
|
||||
}
|
||||
// Three signature cases:
|
||||
// io.input()
|
||||
// io.input(file)
|
||||
// io.input(name)
|
||||
if (lua_gettop(L) == 0)
|
||||
{
|
||||
// We have no arguments, call realio.input()
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
if (output)
|
||||
{
|
||||
lua_getfield(L, -1, "output");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_getfield(L, -1, "input");
|
||||
}
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_call(L, 0, 1);
|
||||
return 1;
|
||||
}
|
||||
if (lua_gettop(L) != 1)
|
||||
{
|
||||
return luaL_error(L, "Too many arguments given to io.input().");
|
||||
}
|
||||
// Now check if we have a file or name
|
||||
auto *p = luaL_testudata(L, 1, LUA_FILEHANDLE);
|
||||
if (p == nullptr)
|
||||
{
|
||||
// this is not a file handle, send it to open
|
||||
luaL_getsubtable(L, LUA_REGISTRYINDEX, REG_C2_IO_NAME);
|
||||
lua_getfield(L, -1, "open");
|
||||
lua_remove(L, -2); // remove io
|
||||
|
||||
lua_pushvalue(L, 1); // dupe arg
|
||||
if (output)
|
||||
{
|
||||
lua_pushstring(L, "w");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_pushstring(L, "r");
|
||||
}
|
||||
lua_call(L, 2, 1); // call ourio.open(arg1, 'r'|'w')
|
||||
// if this isn't a string ourio.open errors
|
||||
|
||||
// this leaves us with:
|
||||
// 1. arg
|
||||
// 2. new_file
|
||||
lua_remove(L, 1); // remove arg, replacing it with new_file
|
||||
}
|
||||
|
||||
// file handle, pass it off to realio.input
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, REG_REAL_IO_NAME);
|
||||
if (output)
|
||||
{
|
||||
lua_getfield(L, -1, "output");
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_getfield(L, -1, "input");
|
||||
}
|
||||
lua_remove(L, -2); // remove LUA_REGISTRYINDEX[REAL_IO_NAME]
|
||||
lua_pushvalue(L, 1); // duplicate arg
|
||||
lua_call(L, 1, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int io_input(lua_State *L)
|
||||
{
|
||||
return globalFileCommon(L, false);
|
||||
}
|
||||
|
||||
int io_output(lua_State *L)
|
||||
{
|
||||
return globalFileCommon(L, true);
|
||||
}
|
||||
|
||||
int io_close(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.close. Expected one or zero.");
|
||||
}
|
||||
if (lua_gettop(L) == 0)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
}
|
||||
lua_getfield(L, -1, "close");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int io_flush(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.flush. Expected one or zero.");
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
lua_getfield(L, -1, "flush");
|
||||
lua_pushvalue(L, -2);
|
||||
lua_call(L, 1, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int io_read(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) > 1)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "Too many arguments for io.read. Expected one or zero.");
|
||||
}
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_input");
|
||||
lua_getfield(L, -1, "read");
|
||||
lua_insert(L, 1);
|
||||
lua_insert(L, 2);
|
||||
lua_call(L, lua_gettop(L) - 1, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int io_write(lua_State *L)
|
||||
{
|
||||
lua_getfield(L, LUA_REGISTRYINDEX, "_IO_output");
|
||||
lua_getfield(L, -1, "write");
|
||||
lua_insert(L, 1);
|
||||
lua_insert(L, 2);
|
||||
// (input)
|
||||
// (input).read
|
||||
// args
|
||||
lua_call(L, lua_gettop(L) - 1, 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int io_popen(lua_State *L)
|
||||
{
|
||||
return luaL_error(L, "io.popen: This function is a stub!");
|
||||
}
|
||||
|
||||
int io_tmpfile(lua_State *L)
|
||||
{
|
||||
return luaL_error(L, "io.tmpfile: This function is a stub!");
|
||||
}
|
||||
|
||||
// NOLINTEND(*vararg)
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
struct lua_State;
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
// These functions are exposed as `_G.io`, they are wrappers for native Lua functionality.
|
||||
|
||||
const char *const REG_REAL_IO_NAME = "real_lua_io_lib";
|
||||
const char *const REG_C2_IO_NAME = "c2io";
|
||||
|
||||
/**
|
||||
* Opens a file.
|
||||
* If given a relative path, it will be relative to
|
||||
* c2datadir/Plugins/pluginDir/data/
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.open
|
||||
*
|
||||
* @lua@param filename string
|
||||
* @lua@param mode nil|"r"|"w"|"a"|"r+"|"w+"|"a+"
|
||||
* @exposed io.open
|
||||
*/
|
||||
int io_open(lua_State *L);
|
||||
|
||||
/**
|
||||
* Equivalent to io.input():lines("l") or a specific iterator over given file
|
||||
* If given a relative path, it will be relative to
|
||||
* c2datadir/Plugins/pluginDir/data/
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.lines
|
||||
*
|
||||
* @lua@param filename nil|string
|
||||
* @lua@param ...
|
||||
* @exposed io.lines
|
||||
*/
|
||||
int io_lines(lua_State *L);
|
||||
|
||||
/**
|
||||
* Opens a file and sets it as default input or if given no arguments returns the default input.
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.input
|
||||
*
|
||||
* @lua@param fileorname nil|string|FILE*
|
||||
* @lua@return nil|FILE*
|
||||
* @exposed io.input
|
||||
*/
|
||||
int io_input(lua_State *L);
|
||||
|
||||
/**
|
||||
* Opens a file and sets it as default output or if given no arguments returns the default output
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.output
|
||||
*
|
||||
* @lua@param fileorname nil|string|FILE*
|
||||
* @lua@return nil|FILE*
|
||||
* @exposed io.output
|
||||
*/
|
||||
int io_output(lua_State *L);
|
||||
|
||||
/**
|
||||
* Closes given file or io.output() if not given.
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.close
|
||||
*
|
||||
* @lua@param nil|FILE*
|
||||
* @exposed io.close
|
||||
*/
|
||||
int io_close(lua_State *L);
|
||||
|
||||
/**
|
||||
* Flushes the buffer for given file or io.output() if not given.
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.flush
|
||||
*
|
||||
* @lua@param nil|FILE*
|
||||
* @exposed io.flush
|
||||
*/
|
||||
int io_flush(lua_State *L);
|
||||
|
||||
/**
|
||||
* Reads some data from the default input file
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.read
|
||||
*
|
||||
* @lua@param nil|string
|
||||
* @exposed io.read
|
||||
*/
|
||||
int io_read(lua_State *L);
|
||||
|
||||
/**
|
||||
* Writes some data to the default output file
|
||||
* See https://www.lua.org/manual/5.4/manual.html#pdf-io.write
|
||||
*
|
||||
* @lua@param nil|string
|
||||
* @exposed io.write
|
||||
*/
|
||||
int io_write(lua_State *L);
|
||||
|
||||
int io_popen(lua_State *L);
|
||||
int io_tmpfile(lua_State *L);
|
||||
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
} // namespace chatterino::lua::api
|
||||
#endif
|
||||
Reference in New Issue
Block a user