blob: 023333da7eb7641bcd76d5ef168a7c84b9b6cdfd [file] [log] [blame]
/*
* Copyright (c) 2021, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "LuaEngine.h"
#include "BundleWrapper.h"
#include <android-base/logging.h>
#include <com/android/car/telemetry/scriptexecutorinterface/IScriptExecutorConstants.h>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
#include "lualib.h"
}
namespace com {
namespace android {
namespace car {
namespace scriptexecutor {
using ::com::android::car::telemetry::scriptexecutorinterface::IScriptExecutorConstants;
namespace {
enum LuaNumReturnedResults {
ZERO_RETURNED_RESULTS = 0,
};
// TODO(199415783): Revisit the topic of limits to potentially move it to standalone file.
constexpr int MAX_ARRAY_SIZE = 1000;
// Helper method that goes over Lua table fields one by one and populates PersistableBundle
// object wrapped in BundleWrapper.
// It is assumed that Lua table is located on top of the Lua stack.
//
// Returns false if the conversion encountered unrecoverable error.
// Otherwise, returns true for success.
// TODO(b/200849134): Refactor this function.
bool convertLuaTableToBundle(lua_State* lua, BundleWrapper* bundleWrapper,
ScriptExecutorListener* listener) {
// Iterate over Lua table which is expected to be at the top of Lua stack.
// lua_next call pops the key from the top of the stack and finds the next
// key-value pair. It returns 0 if the next pair was not found.
// More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
lua_pushnil(lua); // First key is a null value.
while (lua_next(lua, /* index = */ -2) != 0) {
// 'key' is at index -2 and 'value' is at index -1
// -1 index is the top of the stack.
// remove 'value' and keep 'key' for next iteration
// Process each key-value depending on a type and push it to Java PersistableBundle.
// TODO(199531928): Consider putting limits on key sizes as well.
const char* key = lua_tostring(lua, /* index = */ -2);
if (lua_isboolean(lua, /* index = */ -1)) {
bundleWrapper->putBoolean(key, static_cast<bool>(lua_toboolean(lua, /* index = */ -1)));
} else if (lua_isinteger(lua, /* index = */ -1)) {
bundleWrapper->putLong(key, static_cast<int64_t>(lua_tointeger(lua, /* index = */ -1)));
} else if (lua_isnumber(lua, /* index = */ -1)) {
bundleWrapper->putDouble(key, static_cast<double>(lua_tonumber(lua, /* index = */ -1)));
} else if (lua_isstring(lua, /* index = */ -1)) {
// TODO(199415783): We need to have a limit on how long these strings could be.
bundleWrapper->putString(key, lua_tostring(lua, /* index = */ -1));
} else if (lua_istable(lua, /* index =*/-1)) {
// Lua uses tables to represent an array.
// TODO(199438375): Document to users that we expect tables to be either only indexed or
// keyed but not both. If the table contains consecutively indexed values starting from
// 1, we will treat it as an array. lua_rawlen call returns the size of the indexed
// part. We copy this part into an array, but any keyed values in this table are
// ignored. There is a test that documents this current behavior. If a user wants a
// nested table to be represented by a PersistableBundle object, they must make sure
// that the nested table does not contain indexed data, including no key=1.
const auto kTableLength = lua_rawlen(lua, -1);
if (kTableLength > MAX_ARRAY_SIZE) {
std::ostringstream out;
out << "Returned table " << key << " exceeds maximum allowed size of "
<< MAX_ARRAY_SIZE
<< " elements. This key-value cannot be unpacked successfully. This error "
"is unrecoverable.";
listener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
out.str().c_str(), "");
return false;
}
if (kTableLength <= 0) {
std::ostringstream out;
out << "A value with key=" << key
<< " appears to be a nested table that does not represent an array of data. "
"Such nested tables are not supported yet. This script error is "
"unrecoverable.";
listener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
out.str().c_str(), "");
return false;
}
std::vector<int64_t> longArray;
std::vector<std::string> stringArray;
int originalLuaType = LUA_TNIL;
for (int i = 0; i < kTableLength; i++) {
lua_rawgeti(lua, -1, i + 1);
// Lua allows arrays to have values of varying type. We need to force all Lua
// arrays to stick to single type within the same array. We use the first value
// in the array to determine the type of all values in the array that follow
// after. If the second, third, etc element of the array does not match the type
// of the first element we stop the extraction and return an error via a
// callback.
if (i == 0) {
originalLuaType = lua_type(lua, /* index = */ -1);
}
int currentType = lua_type(lua, /* index= */ -1);
if (currentType != originalLuaType) {
std::ostringstream out;
out << "Returned Lua arrays must have elements of the same type. Returned "
"table with key="
<< key << " has the first element of type=" << originalLuaType
<< ", but the element at index=" << i + 1 << " has type=" << currentType
<< ". Integer type codes are defined in lua.h file. This error is "
"unrecoverable.";
listener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
out.str().c_str(), "");
lua_pop(lua, 1);
return false;
}
switch (currentType) {
case LUA_TNUMBER:
if (!lua_isinteger(lua, /* index = */ -1)) {
LOG(WARNING) << "Floating array types are not supported yet. Skipping.";
} else {
longArray.push_back(lua_tointeger(lua, /* index = */ -1));
}
break;
case LUA_TSTRING:
// TODO(b/200833728): Investigate optimizations to minimize string
// copying. For example, populate JNI object array one element at a
// time, as we go.
stringArray.push_back(lua_tostring(lua, /* index = */ -1));
break;
default:
LOG(WARNING) << "Lua array with elements of type=" << currentType
<< " are not supported. Skipping.";
}
lua_pop(lua, 1);
}
switch (originalLuaType) {
case LUA_TNUMBER:
bundleWrapper->putLongArray(key, longArray);
break;
case LUA_TSTRING:
bundleWrapper->putStringArray(key, stringArray);
break;
}
} else {
// not supported yet...
// TODO(199439259): Instead of logging here, log and send to user instead, and continue
// unpacking the rest of the table.
LOG(WARNING) << "key=" << key << " has a Lua type which is not supported yet. "
<< "The bundle object will not have this key-value pair.";
}
// Pop value from the stack, keep the key for the next iteration.
lua_pop(lua, 1);
// The key is at index -1, the table is at index -2 now.
}
return true;
}
} // namespace
ScriptExecutorListener* LuaEngine::sListener = nullptr;
LuaEngine::LuaEngine() {
// Instantiate Lua environment
mLuaState = luaL_newstate();
luaL_openlibs(mLuaState);
}
LuaEngine::~LuaEngine() {
lua_close(mLuaState);
}
lua_State* LuaEngine::getLuaState() {
return mLuaState;
}
void LuaEngine::resetListener(ScriptExecutorListener* listener) {
if (sListener != nullptr) {
delete sListener;
}
sListener = listener;
}
int LuaEngine::loadScript(const char* scriptBody) {
// As the first step in Lua script execution we want to load
// the body of the script into Lua stack and have it processed by Lua
// to catch any errors.
// More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
// If error, pushes the error object into the stack.
const auto status = luaL_dostring(mLuaState, scriptBody);
if (status) {
// Removes error object from the stack.
// Lua stack must be properly maintained due to its limited size,
// ~20 elements and its critical function because all interaction with
// Lua happens via the stack.
// Starting read about Lua stack: https://www.lua.org/pil/24.2.html
// TODO(b/192284232): add test case to trigger this.
lua_pop(mLuaState, 1);
return status;
}
// Register limited set of reserved methods for Lua to call native side.
lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
lua_register(mLuaState, "on_script_finished", LuaEngine::onScriptFinished);
lua_register(mLuaState, "on_error", LuaEngine::onError);
return status;
}
bool LuaEngine::pushFunction(const char* functionName) {
// Interaction between native code and Lua happens via Lua stack.
// In such model, a caller first pushes the name of the function
// that needs to be called, followed by the function's input
// arguments, one input value pushed at a time.
// More info: https://www.lua.org/pil/24.2.html
lua_getglobal(mLuaState, functionName);
const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
// TODO(b/192284785): add test case for wrong function name in Lua.
if (status == 0) lua_pop(mLuaState, 1);
return status;
}
int LuaEngine::run() {
// Performs blocking call of the provided Lua function. Assumes all
// input arguments are in the Lua stack as well in proper order.
// On how to call Lua functions: https://www.lua.org/pil/25.2.html
// Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
// TODO(b/192284612): add test case for failed call.
return lua_pcall(mLuaState, /* nargs= */ 2, /* nresults= */ 0, /*errfunc= */ 0);
}
int LuaEngine::onSuccess(lua_State* lua) {
// Any script we run can call on_success only with a single argument of Lua table type.
if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_success can push only a single parameter from Lua - a Lua table",
"");
return ZERO_RETURNED_RESULTS;
}
// Helper object to create and populate Java PersistableBundle object.
BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
if (convertLuaTableToBundle(lua, &bundleWrapper, sListener)) {
// Forward the populated Bundle object to Java callback.
sListener->onSuccess(bundleWrapper.getBundle());
}
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
int LuaEngine::onScriptFinished(lua_State* lua) {
// Any script we run can call on_success only with a single argument of Lua table type.
if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_script_finished can push only a single parameter from Lua - a Lua "
"table",
"");
return ZERO_RETURNED_RESULTS;
}
// Helper object to create and populate Java PersistableBundle object.
BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
if (convertLuaTableToBundle(lua, &bundleWrapper, sListener)) {
// Forward the populated Bundle object to Java callback.
sListener->onScriptFinished(bundleWrapper.getBundle());
}
// We explicitly must tell Lua how many results we return, which is 0 in this case.
// More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
return ZERO_RETURNED_RESULTS;
}
int LuaEngine::onError(lua_State* lua) {
// Any script we run can call on_error only with a single argument of Lua string type.
if (lua_gettop(lua) != 1 || !lua_isstring(lua, /* index = */ -1)) {
sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
"on_error can push only a single string parameter from Lua", "");
return ZERO_RETURNED_RESULTS;
}
sListener->onError(IScriptExecutorConstants::ERROR_TYPE_LUA_SCRIPT_ERROR,
lua_tostring(lua, /* index = */ -1), /* stackTrace =*/"");
return ZERO_RETURNED_RESULTS;
}
} // namespace scriptexecutor
} // namespace car
} // namespace android
} // namespace com