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 "JniUtils.h"
18
19 #include "nativehelper/scoped_local_ref.h"
20
21 namespace com {
22 namespace android {
23 namespace car {
24 namespace scriptexecutor {
25
26 // TODO(b/199415783): Revisit the topic of limits to potentially move it to standalone file.
27 constexpr int MAX_ARRAY_SIZE = 1000;
28
pushBundleToLuaTable(JNIEnv * env,lua_State * lua,jobject bundle)29 void pushBundleToLuaTable(JNIEnv* env, lua_State* lua, jobject bundle) {
30 lua_newtable(lua);
31 // null bundle object is allowed. We will treat it as an empty table.
32 if (bundle == nullptr) {
33 return;
34 }
35
36 // TODO(b/188832769): Consider caching some of these JNI references for
37 // performance reasons.
38 ScopedLocalRef<jclass> persistableBundleClass(env,
39 env->FindClass("android/os/PersistableBundle"));
40 jmethodID getKeySetMethod =
41 env->GetMethodID(persistableBundleClass.get(), "keySet", "()Ljava/util/Set;");
42 ScopedLocalRef<jobject> keys(env, env->CallObjectMethod(bundle, getKeySetMethod));
43 ScopedLocalRef<jclass> setClass(env, env->FindClass("java/util/Set"));
44 jmethodID iteratorMethod =
45 env->GetMethodID(setClass.get(), "iterator", "()Ljava/util/Iterator;");
46 ScopedLocalRef<jobject> keySetIteratorObject(env,
47 env->CallObjectMethod(keys.get(), iteratorMethod));
48
49 ScopedLocalRef<jclass> iteratorClass(env, env->FindClass("java/util/Iterator"));
50 jmethodID hasNextMethod = env->GetMethodID(iteratorClass.get(), "hasNext", "()Z");
51 jmethodID nextMethod = env->GetMethodID(iteratorClass.get(), "next", "()Ljava/lang/Object;");
52
53 ScopedLocalRef<jclass> booleanClass(env, env->FindClass("java/lang/Boolean"));
54 ScopedLocalRef<jclass> integerClass(env, env->FindClass("java/lang/Integer"));
55 ScopedLocalRef<jclass> longClass(env, env->FindClass("java/lang/Long"));
56 ScopedLocalRef<jclass> numberClass(env, env->FindClass("java/lang/Number"));
57 ScopedLocalRef<jclass> stringClass(env, env->FindClass("java/lang/String"));
58 ScopedLocalRef<jclass> booleanArrayClass(env, env->FindClass("[Z"));
59 ScopedLocalRef<jclass> intArrayClass(env, env->FindClass("[I"));
60 ScopedLocalRef<jclass> longArrayClass(env, env->FindClass("[J"));
61 ScopedLocalRef<jclass> doubleArrayClass(env, env->FindClass("[D"));
62 ScopedLocalRef<jclass> stringArrayClass(env, env->FindClass("[Ljava/lang/String;"));
63
64 jmethodID getMethod = env->GetMethodID(persistableBundleClass.get(), "get",
65 "(Ljava/lang/String;)Ljava/lang/Object;");
66
67 // Iterate over key set of the bundle one key at a time.
68 while (env->CallBooleanMethod(keySetIteratorObject.get(), hasNextMethod)) {
69 // Read the value object that corresponds to this key.
70 ScopedLocalRef<jstring> key(env,
71 (jstring)env->CallObjectMethod(keySetIteratorObject.get(),
72 nextMethod));
73 ScopedLocalRef<jobject> value(env, env->CallObjectMethod(bundle, getMethod, key.get()));
74
75 // Get the value of the type, extract it accordingly from the bundle and
76 // push the extracted value and the key to the Lua table.
77 if (env->IsInstanceOf(value.get(), booleanClass.get())) {
78 jmethodID boolMethod = env->GetMethodID(booleanClass.get(), "booleanValue", "()Z");
79 bool boolValue = static_cast<bool>(env->CallBooleanMethod(value.get(), boolMethod));
80 lua_pushboolean(lua, boolValue);
81 } else if (env->IsInstanceOf(value.get(), integerClass.get())) {
82 jmethodID intMethod = env->GetMethodID(integerClass.get(), "intValue", "()I");
83 lua_pushinteger(lua, env->CallIntMethod(value.get(), intMethod));
84 } else if (env->IsInstanceOf(value.get(), longClass.get())) {
85 jmethodID longMethod = env->GetMethodID(longClass.get(), "longValue", "()J");
86 lua_pushinteger(lua, env->CallLongMethod(value.get(), longMethod));
87 } else if (env->IsInstanceOf(value.get(), numberClass.get())) {
88 // Condense other numeric types using one class. Because lua supports only
89 // integer or double, and we handled integer in previous if clause.
90 jmethodID numberMethod = env->GetMethodID(numberClass.get(), "doubleValue", "()D");
91 /* Pushes a double onto the stack */
92 lua_pushnumber(lua, env->CallDoubleMethod(value.get(), numberMethod));
93 } else if (env->IsInstanceOf(value.get(), stringClass.get())) {
94 // Produces a string in Modified UTF-8 encoding. Any null character
95 // inside the original string is converted into two-byte encoding.
96 // This way we can directly use the output of GetStringUTFChars in C API that
97 // expects a null-terminated string.
98 const char* rawStringValue =
99 env->GetStringUTFChars(static_cast<jstring>(value.get()), nullptr);
100 lua_pushstring(lua, rawStringValue);
101 env->ReleaseStringUTFChars(static_cast<jstring>(value.get()), rawStringValue);
102 } else if (env->IsInstanceOf(value.get(), booleanArrayClass.get())) {
103 jbooleanArray booleanArray = static_cast<jbooleanArray>(value.get());
104 const auto kLength = env->GetArrayLength(booleanArray);
105 // Arrays are represented as a table of sequential elements in Lua.
106 // We are creating a nested table to represent this array. We specify number of elements
107 // in the Java array to preallocate memory accordingly.
108 lua_createtable(lua, kLength, 0);
109 jboolean* rawBooleanArray = env->GetBooleanArrayElements(booleanArray, nullptr);
110 // Fills in the table at stack idx -2 with key value pairs, where key is a
111 // Lua index and value is an integer from the long array at that index
112 for (int i = 0; i < kLength; i++) {
113 lua_pushboolean(lua, rawBooleanArray[i]);
114 lua_rawseti(lua, /* idx= */ -2,
115 i + 1); // lua index starts from 1
116 }
117 // JNI_ABORT is used because we do not need to copy back elements.
118 env->ReleaseBooleanArrayElements(booleanArray, rawBooleanArray, JNI_ABORT);
119 } else if (env->IsInstanceOf(value.get(), intArrayClass.get())) {
120 jintArray intArray = static_cast<jintArray>(value.get());
121 const auto kLength = env->GetArrayLength(intArray);
122 // Arrays are represented as a table of sequential elements in Lua.
123 // We are creating a nested table to represent this array. We specify number of elements
124 // in the Java array to preallocate memory accordingly.
125 lua_createtable(lua, kLength, 0);
126 jint* rawIntArray = env->GetIntArrayElements(intArray, nullptr);
127 // Fills in the table at stack idx -2 with key value pairs, where key is a
128 // Lua index and value is an integer from the int array at that index
129 for (int i = 0; i < kLength; i++) {
130 // Stack at index -1 is rawIntArray[i] after this push.
131 lua_pushinteger(lua, rawIntArray[i]);
132 lua_rawseti(lua, /* idx= */ -2,
133 i + 1); // lua index starts from 1
134 }
135 // JNI_ABORT is used because we do not need to copy back elements.
136 env->ReleaseIntArrayElements(intArray, rawIntArray, JNI_ABORT);
137 } else if (env->IsInstanceOf(value.get(), longArrayClass.get())) {
138 jlongArray longArray = static_cast<jlongArray>(value.get());
139 const auto kLength = env->GetArrayLength(longArray);
140 // Arrays are represented as a table of sequential elements in Lua.
141 // We are creating a nested table to represent this array. We specify number of elements
142 // in the Java array to preallocate memory accordingly.
143 lua_createtable(lua, kLength, 0);
144 jlong* rawLongArray = env->GetLongArrayElements(longArray, nullptr);
145 // Fills in the table at stack idx -2 with key value pairs, where key is a
146 // Lua index and value is an integer from the long array at that index
147 for (int i = 0; i < kLength; i++) {
148 lua_pushinteger(lua, rawLongArray[i]);
149 lua_rawseti(lua, /* idx= */ -2,
150 i + 1); // lua index starts from 1
151 }
152 // JNI_ABORT is used because we do not need to copy back elements.
153 env->ReleaseLongArrayElements(longArray, rawLongArray, JNI_ABORT);
154 } else if (env->IsInstanceOf(value.get(), doubleArrayClass.get())) {
155 jdoubleArray doubleArray = static_cast<jdoubleArray>(value.get());
156 const auto kLength = env->GetArrayLength(doubleArray);
157 // Arrays are represented as a table of sequential elements in Lua.
158 // We are creating a nested table to represent this array. We specify number of elements
159 // in the Java array to preallocate memory accordingly.
160 lua_createtable(lua, kLength, 0);
161 jdouble* rawDoubleArray = env->GetDoubleArrayElements(doubleArray, nullptr);
162 // Fills in the table at stack idx -2 with key value pairs, where key is a
163 // Lua index and value is an double from the double array at that index
164 for (int i = 0; i < kLength; i++) {
165 lua_pushnumber(lua, rawDoubleArray[i]);
166 lua_rawseti(lua, /* idx= */ -2,
167 i + 1); // lua index starts from 1
168 }
169 env->ReleaseDoubleArrayElements(doubleArray, rawDoubleArray, JNI_ABORT);
170 } else if (env->IsInstanceOf(value.get(), stringArrayClass.get())) {
171 jobjectArray stringArray = static_cast<jobjectArray>(value.get());
172 const auto kLength = env->GetArrayLength(stringArray);
173 // Arrays are represented as a table of sequential elements in Lua.
174 // We are creating a nested table to represent this array. We specify number of elements
175 // in the Java array to preallocate memory accordingly.
176 lua_createtable(lua, kLength, 0);
177 // Fills in the table at stack idx -2 with key value pairs, where key is a Lua index and
178 // value is an string value extracted from the object array at that index
179 for (int i = 0; i < kLength; i++) {
180 ScopedLocalRef<jobject> localStringRef(env,
181 env->GetObjectArrayElement(stringArray, i));
182 jstring element = static_cast<jstring>(localStringRef.get());
183 const char* rawStringValue = env->GetStringUTFChars(element, nullptr);
184 lua_pushstring(lua, rawStringValue);
185 env->ReleaseStringUTFChars(element, rawStringValue);
186 // lua index starts from 1
187 lua_rawseti(lua, /* idx= */ -2, i + 1);
188 }
189 } else if (env->IsInstanceOf(value.get(), persistableBundleClass.get())) {
190 jobject bundle = static_cast<jobject>(value.get());
191 // After this call, the lua stack will have 1 new item at the top of the stack: a table
192 // representing the PersistableBundle
193 pushBundleToLuaTable(env, lua, bundle);
194 } else {
195 // Other types are not implemented yet, skipping.
196 continue;
197 }
198
199 const char* rawKey = env->GetStringUTFChars(key.get(), nullptr);
200 // table[rawKey] = value, where value is on top of the stack,
201 // and the table is the next element in the stack.
202 lua_setfield(lua, /* idx= */ -2, rawKey);
203 env->ReleaseStringUTFChars(key.get(), rawKey);
204 }
205 }
206
pushBundleListToLuaTable(JNIEnv * env,lua_State * lua,jobject bundleList)207 void pushBundleListToLuaTable(JNIEnv* env, lua_State* lua, jobject bundleList) {
208 // Creates a new table as the encompassing array to contain the converted bundles.
209 // Pushed to top of stack.
210 lua_newtable(lua);
211
212 ScopedLocalRef<jclass> listClass(env, env->FindClass("java/util/List"));
213 jmethodID sizeMethod = env->GetMethodID(listClass.get(), "size", "()I");
214 jmethodID getMethod = env->GetMethodID(listClass.get(), "get", "(I)Ljava/lang/Object;");
215
216 const auto listSize = env->CallIntMethod(bundleList, sizeMethod);
217 // For each bundle in the bundleList set a converted Lua table into the table array.
218 for (int i = 0; i < listSize; i++) {
219 // Push to stack the index at which the next Lua table will be at. Lua index start at 1.
220 lua_pushnumber(lua, i + 1);
221 // Convert the bundle at i into Lua table and push to top of stack.
222 pushBundleToLuaTable(env, lua, env->CallObjectMethod(bundleList, getMethod, i));
223 // table[k] = v, table should be at the given index (-3), and expects v the value to be
224 // at the top of the stack, and k the key to be just below the top.
225 lua_settable(lua, /* idx= */ -3);
226 }
227 }
228
convertLuaTableToBundle(JNIEnv * env,lua_State * lua,BundleWrapper * bundleWrapper)229 Result<void> convertLuaTableToBundle(JNIEnv* env, lua_State* lua, BundleWrapper* bundleWrapper) {
230 // Iterate over Lua table which is expected to be at the top of Lua stack.
231 // lua_next call pops the key from the top of the stack and finds the next
232 // key-value pair. It returns 0 if the next pair was not found.
233 // More on lua_next in: https://www.lua.org/manual/5.3/manual.html#lua_next
234 lua_pushnil(lua); // First key is a null value, at index -1
235 while (lua_next(lua, /* table index = */ -2) != 0) {
236 // 'key' is at index -2 and 'value' is at index -1
237 // -1 index is the top of the stack.
238 // remove 'value' and keep 'key' for next iteration
239 // Process each key-value depending on a type and push it to Java PersistableBundle.
240 // TODO(b/199531928): Consider putting limits on key sizes as well.
241 const char* key = lua_tostring(lua, /* index = */ -2);
242 Result<void> bundleInsertionResult;
243 if (lua_isboolean(lua, /* index = */ -1)) {
244 bundleInsertionResult =
245 bundleWrapper->putBoolean(key,
246 static_cast<bool>(
247 lua_toboolean(lua, /* index = */ -1)));
248 } else if (lua_isinteger(lua, /* index = */ -1)) {
249 bundleInsertionResult =
250 bundleWrapper->putLong(key,
251 static_cast<int64_t>(
252 lua_tointeger(lua, /* index = */ -1)));
253 } else if (lua_isnumber(lua, /* index = */ -1)) {
254 bundleInsertionResult =
255 bundleWrapper->putDouble(key,
256 static_cast<double>(
257 lua_tonumber(lua, /* index = */ -1)));
258 } else if (lua_isstring(lua, /* index = */ -1)) {
259 // TODO(b/199415783): We need to have a limit on how long these strings could be.
260 bundleInsertionResult =
261 bundleWrapper->putString(key, lua_tostring(lua, /* index = */ -1));
262 } else if (lua_istable(lua, /* index =*/-1) && lua_rawlen(lua, -1) > 0) {
263 // Lua uses tables to represent arrays and PersistableBundles.
264 // If lua_rawlen is greater than 0, this table is a sequence, which means it is an
265 // array.
266
267 // TODO(b/199438375): Document to users that we expect tables to be either only indexed
268 // or keyed but not both. If the table contains consecutively indexed values starting
269 // from 1, we will treat it as an array. lua_rawlen call returns the size of the indexed
270 // part. We copy this part into an array, but any keyed values in this table are
271 // ignored. There is a test that documents this current behavior. If a user wants a
272 // nested table to be represented by a PersistableBundle object, they must make sure
273 // that the nested table does not contain indexed data, including no key=1.
274 const auto kTableLength = lua_rawlen(lua, -1);
275 if (kTableLength > MAX_ARRAY_SIZE) {
276 return Error()
277 << "Returned table " << key << " exceeds maximum allowed size of "
278 << MAX_ARRAY_SIZE
279 << " elements. This key-value cannot be unpacked successfully. This error "
280 "is unrecoverable.";
281 }
282
283 std::vector<unsigned char> boolArray;
284 std::vector<double> doubleArray;
285 std::vector<int64_t> longArray;
286 std::vector<std::string> stringArray;
287 int originalLuaType = LUA_TNIL;
288 for (int i = 0; i < kTableLength; i++) {
289 lua_rawgeti(lua, -1, i + 1);
290 // Lua allows arrays to have values of varying type. We need to force all Lua
291 // arrays to stick to single type within the same array. We use the first value
292 // in the array to determine the type of all values in the array that follow
293 // after. If the second, third, etc element of the array does not match the type
294 // of the first element we stop the extraction and return an error via a
295 // callback.
296 if (i == 0) {
297 originalLuaType = lua_type(lua, /* index = */ -1);
298 }
299 int currentType = lua_type(lua, /* index= */ -1);
300 if (currentType != originalLuaType) {
301 return Error()
302 << "Returned Lua arrays must have elements of the same type. Returned "
303 "table with key="
304 << key << " has the first element of type=" << originalLuaType
305 << ", but the element at index=" << i + 1 << " has type=" << currentType
306 << ". Integer type codes are defined in lua.h file. This error is "
307 "unrecoverable.";
308 }
309 switch (currentType) {
310 case LUA_TBOOLEAN:
311 boolArray.push_back(
312 static_cast<unsigned char>(lua_toboolean(lua, /* index = */ -1)));
313 break;
314 case LUA_TNUMBER:
315 if (lua_isinteger(lua, /* index = */ -1)) {
316 longArray.push_back(lua_tointeger(lua, /* index = */ -1));
317 } else {
318 doubleArray.push_back(lua_tonumber(lua, /* index = */ -1));
319 }
320 break;
321 case LUA_TSTRING:
322 // TODO(b/200833728): Investigate optimizations to minimize string
323 // copying. For example, populate JNI object array one element at a
324 // time, as we go.
325 stringArray.push_back(lua_tostring(lua, /* index = */ -1));
326 break;
327 default:
328 return Error() << "Returned value for key=" << key
329 << " is an array with values of type="
330 << lua_typename(lua, lua_type(lua, /* index = */ -1))
331 << ", which is not supported yet.";
332 }
333 lua_pop(lua, 1);
334 }
335 switch (originalLuaType) {
336 case LUA_TBOOLEAN:
337 bundleInsertionResult = bundleWrapper->putBooleanArray(key, boolArray);
338 break;
339 case LUA_TNUMBER:
340 if (longArray.size() > 0) {
341 bundleInsertionResult = bundleWrapper->putLongArray(key, longArray);
342 } else {
343 bundleInsertionResult = bundleWrapper->putDoubleArray(key, doubleArray);
344 }
345 break;
346 case LUA_TSTRING:
347 bundleInsertionResult = bundleWrapper->putStringArray(key, stringArray);
348 break;
349 }
350 } else if (lua_istable(lua, /* index =*/-1)) {
351 // If the Lua table is not a sequence, i.e., it is a table with string keys, then it is
352 // a PersistableBundle
353 BundleWrapper nestedBundleWrapper(env);
354 // After this line, the Lua stack is unchanged, so the top of the stack is still a
355 // table, but the nestedBundleWrapper will be populated
356 const auto status = convertLuaTableToBundle(env, lua, &nestedBundleWrapper);
357 if (!status.ok()) {
358 return Error() << "Failed to parse nested tables into nested PersistableBundles";
359 }
360 bundleInsertionResult = bundleWrapper->putPersistableBundle(key, nestedBundleWrapper);
361 } else {
362 return Error() << "key=" << key << " has a Lua type="
363 << lua_typename(lua, lua_type(lua, /* index = */ -1))
364 << ", which is not supported yet.";
365 }
366 // Pop value from the stack, keep the key for the next iteration.
367 lua_pop(lua, 1);
368 // The key is at index -1, the table is at index -2 now.
369
370 // Check if insertion of the current key-value into the bundle was successful. If not,
371 // fail-fast out of this extraction routine.
372 if (!bundleInsertionResult.ok()) {
373 return bundleInsertionResult;
374 }
375 }
376 return {}; // ok result
377 }
378
379 } // namespace scriptexecutor
380 } // namespace car
381 } // namespace android
382 } // namespace com
383