1 /*
2  * Copyright (c) 2021, The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include "LuaEngine.h"
18 
19 #include "BundleWrapper.h"
20 #include "JniUtils.h"
21 #include "jni.h"
22 
23 #include <android-base/logging.h>
24 
25 #include <sstream>
26 #include <string>
27 #include <utility>
28 #include <vector>
29 
30 extern "C" {
31 #include "lauxlib.h"
32 #include "lua.h"
33 #include "lualib.h"
34 }
35 
36 namespace com {
37 namespace android {
38 namespace car {
39 namespace scriptexecutor {
40 
41 namespace {
42 
43 enum LuaNumReturnedResults {
44     ZERO_RETURNED_RESULTS = 0,
45 };
46 
47 // Prefix for logging messages coming from lua script.
48 const char kLuaLogTag[] = "LUA: ";
49 
50 }  // namespace
51 
52 ScriptExecutorListener* LuaEngine::sListener = nullptr;
53 
LuaEngine()54 LuaEngine::LuaEngine() {
55     // Instantiate Lua environment
56     mLuaState = luaL_newstate();
57     luaL_openlibs(mLuaState);
58 }
59 
~LuaEngine()60 LuaEngine::~LuaEngine() {
61     lua_close(mLuaState);
62 }
63 
getLuaState()64 lua_State* LuaEngine::getLuaState() {
65     return mLuaState;
66 }
67 
resetListener(ScriptExecutorListener * listener)68 void LuaEngine::resetListener(ScriptExecutorListener* listener) {
69     if (sListener != nullptr) {
70         delete sListener;
71     }
72     sListener = listener;
73 }
74 
loadScript(const char * scriptBody)75 int LuaEngine::loadScript(const char* scriptBody) {
76     // As the first step in Lua script execution we want to load
77     // the body of the script into Lua stack and have it processed by Lua
78     // to catch any errors.
79     // More on luaL_dostring: https://www.lua.org/manual/5.3/manual.html#lual_dostring
80     // If error, pushes the error object into the stack.
81     const auto status = luaL_dostring(mLuaState, scriptBody);
82     if (status) {
83         // Removes error object from the stack.
84         // Lua stack must be properly maintained due to its limited size,
85         // ~20 elements and its critical function because all interaction with
86         // Lua happens via the stack.
87         // Starting read about Lua stack: https://www.lua.org/pil/24.2.html
88         const char* error = lua_tostring(mLuaState, -1);
89         lua_pop(mLuaState, 1);
90         std::ostringstream out;
91         out << "Error encountered while loading the script. A possible cause could be syntax "
92                "errors in the script. Error: "
93             << error;
94         sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
95         return status;
96     }
97 
98     // Register limited set of reserved methods for Lua to call native side.
99     lua_register(mLuaState, "log", LuaEngine::scriptLog);
100     lua_register(mLuaState, "on_success", LuaEngine::onSuccess);
101     lua_register(mLuaState, "on_script_finished", LuaEngine::onScriptFinished);
102     lua_register(mLuaState, "on_error", LuaEngine::onError);
103     lua_register(mLuaState, "on_metrics_report", LuaEngine::onMetricsReport);
104     return status;
105 }
106 
pushFunction(const char * functionName)107 int LuaEngine::pushFunction(const char* functionName) {
108     // Interaction between native code and Lua happens via Lua stack.
109     // In such model, a caller first pushes the name of the function
110     // that needs to be called, followed by the function's input
111     // arguments, one input value pushed at a time.
112     // More info: https://www.lua.org/pil/24.2.html
113     lua_getglobal(mLuaState, functionName);
114     const auto status = lua_isfunction(mLuaState, /*idx= */ -1);
115     if (status == 0) {
116         lua_pop(mLuaState, 1);
117         std::ostringstream out;
118         out << "Wrong function name. Provided functionName=" << functionName
119             << " does not correspond to any function in the provided script";
120         sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(), "");
121     }
122     return status;
123 }
124 
run()125 int LuaEngine::run() {
126     // Performs blocking call of the provided Lua function. Assumes all
127     // input arguments are in the Lua stack as well in proper order.
128     // On how to call Lua functions: https://www.lua.org/pil/25.2.html
129     // Doc on lua_pcall: https://www.lua.org/manual/5.3/manual.html#lua_pcall
130     int n_args = 2;
131     int n_results = 0;
132 
133     // pushes the "debug" on top of the stack, so now "debug" is at index -1
134     lua_getglobal(mLuaState, "debug");
135 
136     // pushes "traceback" as debug[traceback] because "debug" is the value at given index -1
137     lua_getfield(mLuaState, -1, "traceback");
138 
139     // removing value "debug" from stack as we only need debug.traceback which is at index -1
140     lua_remove(mLuaState, -2);
141 
142     // We need to insert err_handler (debug.traceback) before all arguments and function.
143     // Current indices (starting from top of stack): debug.traceback (-1), arg2 (-2), arg1 (-3 ==
144     // -n_args-1), function (-4 == -n_args-2) After insert (starting from top of stack): arg2 (-1),
145     // arg1 (-2 == -n_args), function (-3 == -n_args-1), debug.traceback (-4 == -n_args-2) so, we
146     // will insert error_handler at index : (-n_args - 2)
147     int err_handler_index = -n_args - 2;
148     lua_insert(mLuaState, err_handler_index);
149 
150     // After lua_pcall, the function and all arguments are removed from the stack i.e. (n_args+1)
151     // If there is no error then lua_pcall pushes "n_results" elements to the stack.
152     // But in case of error, lua_pcall pushes exactly one element (error message).
153     // So, "error message" will be at top of the stack i.e. "-1".
154     // Therefore, we need to pop error_handler explicitly.
155     // error_handler will be at "-2" index from top of stack after lua_pcall,
156     // but once we pop error_message from top of stack, error_handler's new index will be "-1".
157     int status = lua_pcall(mLuaState, n_args, n_results, err_handler_index);
158     if (status) {
159         const char* error = lua_tostring(mLuaState, -1);
160         std::string s = error;
161 
162         std::string delimiter = "stack traceback:";
163         // because actual delimiter is "\nstack traceback:\n\t"
164         // tried using \n and \t as part of the delimiter , but it did not work.
165         // also tried \\n and \\t in the delimiter, but it did not work.
166         // so to get error_msg string, avoided the \n in front by using dpos -1
167         // and for stack_traceback, avoided \n and \t in the tail by using delimiter.length() + 2.
168         int dpos = s.find(delimiter);
169         std::string error_msg = s.substr(0, dpos - 1);
170         std::string stack_traceback = s.substr(dpos + delimiter.length() + 2);
171 
172         lua_pop(mLuaState, 2);  // pop top 2 elements (error message & error handler) from the stack
173         std::ostringstream out;
174         out << "Error encountered while running the script. The returned error code=" << status
175             << ". Refer to lua.h file of Lua C API library for error code definitions. Error: "
176             << error_msg.c_str();
177         sListener->onError(ERROR_TYPE_LUA_RUNTIME_ERROR, out.str().c_str(),
178                            stack_traceback.c_str());
179     }
180     lua_pop(mLuaState, 1);  // pop top element (error handler) from the stack.
181     return status;
182 }
183 
scriptLog(lua_State * lua)184 int LuaEngine::scriptLog(lua_State* lua) {
185     const auto n = lua_gettop(lua);
186     // Loop through each argument, Lua table indices range from [1 .. N] instead of [0 .. N-1].
187     // Negative indexes are stack positions and positive indexes are argument positions.
188     for (int i = 1; i <= n; i++) {
189         const char* message = lua_tostring(lua, i);
190         LOG(INFO) << kLuaLogTag << message;
191     }
192     return ZERO_RETURNED_RESULTS;
193 }
194 
onSuccess(lua_State * lua)195 int LuaEngine::onSuccess(lua_State* lua) {
196     // Any script we run can call on_success only with a single argument of Lua table type.
197     if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
198         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
199                            "on_success can push only a single parameter from Lua - a Lua table",
200                            "");
201         return ZERO_RETURNED_RESULTS;
202     }
203 
204     // Helper object to create and populate Java PersistableBundle object.
205     BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
206     const auto status = convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &bundleWrapper);
207     if (!status.ok()) {
208         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
209         // We explicitly must tell Lua how many results we return, which is 0 in this case.
210         // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
211         return ZERO_RETURNED_RESULTS;
212     }
213 
214     // Forward the populated Bundle object to Java callback.
215     sListener->onSuccess(bundleWrapper.getBundle());
216 
217     // We explicitly must tell Lua how many results we return, which is 0 in this case.
218     // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
219     return ZERO_RETURNED_RESULTS;
220 }
221 
onScriptFinished(lua_State * lua)222 int LuaEngine::onScriptFinished(lua_State* lua) {
223     // Any script we run can call on_success only with a single argument of Lua table type.
224     if (lua_gettop(lua) != 1 || !lua_istable(lua, /* index =*/-1)) {
225         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
226                            "on_script_finished can push only a single parameter from Lua - a Lua "
227                            "table",
228                            "");
229         return ZERO_RETURNED_RESULTS;
230     }
231 
232     // Helper object to create and populate Java PersistableBundle object.
233     BundleWrapper bundleWrapper(sListener->getCurrentJNIEnv());
234     const auto status = convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &bundleWrapper);
235     if (!status.ok()) {
236         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
237         // We explicitly must tell Lua how many results we return, which is 0 in this case.
238         // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
239         return ZERO_RETURNED_RESULTS;
240     }
241 
242     // Forward the populated Bundle object to Java callback.
243     sListener->onScriptFinished(bundleWrapper.getBundle());
244 
245     // We explicitly must tell Lua how many results we return, which is 0 in this case.
246     // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
247     return ZERO_RETURNED_RESULTS;
248 }
249 
onError(lua_State * lua)250 int LuaEngine::onError(lua_State* lua) {
251     // Any script we run can call on_error only with a single argument of Lua string type.
252     if (lua_gettop(lua) != 1 || !lua_isstring(lua, /* index = */ -1)) {
253         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
254                            "on_error can push only a single string parameter from Lua", "");
255         return ZERO_RETURNED_RESULTS;
256     }
257     sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, lua_tostring(lua, /* index = */ -1),
258                        /* stackTrace =*/"");
259     return ZERO_RETURNED_RESULTS;
260 }
261 
onMetricsReport(lua_State * lua)262 int LuaEngine::onMetricsReport(lua_State* lua) {
263     // Any script we run can call on_metrics_report with at most 2 arguments of Lua table type.
264     if (lua_gettop(lua) > 2 || !lua_istable(lua, /* index =*/-1)) {
265         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
266                            "on_metrics_report should push 1 to 2 parameters of Lua table type. "
267                            "The first table is a metrics report and the second is an optional "
268                            "state to save",
269                            "");
270         return ZERO_RETURNED_RESULTS;
271     }
272 
273     // stack with 2 items:                      stack with 1 item:
274     //     index -1: state_to_persist               index -1: report
275     //     index -2: report
276     // If the stack has 2 items, top of the stack is the state.
277     // If the stack only has one item, top of the stack is the report.
278 
279     // Process the top of the stack. Create helper object and populate Java PersistableBundle
280     // object.
281     BundleWrapper topBundleWrapper(sListener->getCurrentJNIEnv());
282     // If the helper function succeeds, it should not change the stack
283     const auto status =
284             convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &topBundleWrapper);
285     if (!status.ok()) {
286         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, status.error().message().c_str(), "");
287         // We explicitly must tell Lua how many results we return, which is 0 in this case.
288         // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
289         return ZERO_RETURNED_RESULTS;
290     }
291 
292     // If the script provided 1 argument, return now
293     if (lua_gettop(lua) == 1) {
294         sListener->onMetricsReport(topBundleWrapper.getBundle(), nullptr);
295         return ZERO_RETURNED_RESULTS;
296     }
297 
298     // Otherwise the script provided a report and a state
299     // pop the state_to_persist because it has already been processed in topBundleWrapper
300     lua_pop(lua, 1);
301 
302     // check that the second argument is also a table
303     if (!lua_istable(lua, /* index =*/-1)) {
304         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR,
305                            "on_metrics_report should push 1 to 2 parameters of Lua table type. "
306                            "The first table is a metrics report and the second is an optional "
307                            "state to save",
308                            "");
309         return ZERO_RETURNED_RESULTS;
310     }
311 
312     // process the report
313     BundleWrapper bottomBundleWrapper(sListener->getCurrentJNIEnv());
314     const auto statusBottom =
315             convertLuaTableToBundle(sListener->getCurrentJNIEnv(), lua, &bottomBundleWrapper);
316     if (!statusBottom.ok()) {
317         sListener->onError(ERROR_TYPE_LUA_SCRIPT_ERROR, statusBottom.error().message().c_str(), "");
318         // We explicitly must tell Lua how many results we return, which is 0 in this case.
319         // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
320         return ZERO_RETURNED_RESULTS;
321     }
322 
323     // Top of the stack = state, bottom of the stack = report
324     sListener->onMetricsReport(bottomBundleWrapper.getBundle(), topBundleWrapper.getBundle());
325 
326     // We explicitly must tell Lua how many results we return, which is 0 in this case.
327     // More on the topic: https://www.lua.org/manual/5.3/manual.html#lua_CFunction
328     return ZERO_RETURNED_RESULTS;
329 }
330 
331 }  // namespace scriptexecutor
332 }  // namespace car
333 }  // namespace android
334 }  // namespace com
335