From 6161ad065e5d575bd79a1b3ac862354a468e2a03 Mon Sep 17 00:00:00 2001 From: Aleksandr Lyapunov <alyapunov@tarantool.org> Date: Tue, 16 May 2023 01:12:21 +0300 Subject: [PATCH] box: support brackets in name resolution for Lua calls Fefactor lua name resolution, simplify and comment. Add an ability to specify path with brackets, for example in 'box.space[512]:get' or 'box.space["test"]:get'. Only literals (strings and numbers) are supported. Closes #8604 @TarantoolBot document Title: square brackets in procedure resolution for Lua calls Square brackets are now supported in Lua call procedure resolution. This is applicable to `net.box` connection objects `call` method as well as `box.schema.func.call`. Examples of function calls with square brackets can be found in the test to this patch. --- ...s-in-procedure-resolution-for-lua-calls.md | 4 + src/box/lua/call.c | 122 ++++++++++-------- ...rocedure_resolution_for_lua_calls_test.lua | 108 ++++++++++++++++ 3 files changed, 179 insertions(+), 55 deletions(-) create mode 100644 changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md create mode 100644 test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua diff --git a/changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md b/changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md new file mode 100644 index 0000000000..cf69477bba --- /dev/null +++ b/changelogs/unreleased/gh-8604-support-square-brackets-in-procedure-resolution-for-lua-calls.md @@ -0,0 +1,4 @@ +## feature/box + +* Added support for square brackets in procedure resolution for Lua calls + (gh-8604). diff --git a/src/box/lua/call.c b/src/box/lua/call.c index 59c30bb73d..9fbd35c09b 100644 --- a/src/box/lua/call.c +++ b/src/box/lua/call.c @@ -93,74 +93,86 @@ get_call_serializer(void) } /** - * A helper to find a Lua function by name and put it - * on top of the stack. + * A helper to resolve a Lua function by full name, for example like: + * foo.bar['biz']["baz"][3].object:function + * Puts the function on top of the stack, followed by an object (if present). + * Returns number of items pushed (1 or 2) or -1 in case of error (diag is set). */ static int box_lua_find(lua_State *L, const char *name, const char *name_end) { - int index = LUA_GLOBALSINDEX; - int objstack = 0, top = lua_gettop(L); - const char *start = name, *end; - - while ((end = (const char *) memchr(start, '.', name_end - start))) { - lua_checkstack(L, 3); - lua_pushlstring(L, start, end - start); - lua_gettable(L, index); - if (! lua_istable(L, -1)) { - diag_set(ClientError, ER_NO_SUCH_PROC, - name_end - name, name); - return -1; - } - start = end + 1; /* next piece of a.b.c */ - index = lua_gettop(L); /* top of the stack */ - } + lua_checkstack(L, 2); /* No more than 2 entries are needed. */ + int top = lua_gettop(L); - /* box.something:method */ - if ((end = (const char *) memchr(start, ':', name_end - start))) { - lua_checkstack(L, 3); - lua_pushlstring(L, start, end - start); - lua_gettable(L, index); - if (! (lua_istable(L, -1) || - lua_islightuserdata(L, -1) || lua_isuserdata(L, -1) )) { - diag_set(ClientError, ER_NO_SUCH_PROC, - name_end - name, name); - return -1; + /* Take the first token. */ + const char *start = name; + while (start != name_end && *start != '.' && + *start != ':' && *start != '[') + start++; + lua_pushlstring(L, name, start - name); + lua_gettable(L, LUA_GLOBALSINDEX); + + /* Take the rest tokens. */ + while (start != name_end) { + if (!lua_istable(L, -1) && + !lua_islightuserdata(L, -1) && !lua_isuserdata(L, -1)) + goto no_such_proc; + + char delim = *start++; /* skip delimiter. */ + if (delim == '.') { + /* Look for the next token. */ + const char *end = start; + while (end != name_end && *end != '.' && + *end != ':' && *end != '[') + end++; + lua_pushlstring(L, start, end - start); + start = end; + } else if (delim == ':') { + lua_pushlstring(L, start, name_end - start); + lua_gettable(L, -2); /* get function from object. */ + lua_insert(L, -2); /* swap function and object. */ + break; + } else if (delim == '[') { + const char *end = memchr(start, ']', name_end - start); + if (end == NULL) + goto no_such_proc; + + if (end - start >= 2 && start[0] == end[-1] && + (start[0] == '"' || start[0] == '\'')) { + /* Quoted string, just extract it. */ + lua_pushlstring(L, start + 1, end - start - 2); + } else { + /* Must be a number, convert from string. */ + lua_pushlstring(L, start, end - start); + int success; + lua_Number num = lua_tonumberx(L, -1, &success); + if (!success) + goto no_such_proc; + lua_pop(L, 1); + lua_pushnumber(L, num); + } + start = end + 1; /* skip closing bracket. */ + } else { + goto no_such_proc; } - start = end + 1; /* next piece of a.b.c */ - index = lua_gettop(L); /* top of the stack */ - objstack = index - top; + lua_gettable(L, -2); /* get child object from parent object. */ + lua_remove(L, -2); /* drop previous parent object. */ } - - lua_pushlstring(L, start, name_end - start); - lua_gettable(L, index); - if (!lua_isfunction(L, -1) && !lua_istable(L, -1)) { + /* Now at top+1 must be the function, and at top+2 may be the object. */ + assert(lua_gettop(L) - top >= 1 && lua_gettop(L) - top <= 2); + if (!lua_isfunction(L, top + 1) && !lua_istable(L, top + 1)) { /* lua_call or lua_gettable would raise a type error * for us, but our own message is more verbose. */ - diag_set(ClientError, ER_NO_SUCH_PROC, - name_end - name, name); - return -1; + goto no_such_proc; } - /* setting stack that it would contain only - * the function pointer. */ - if (index != LUA_GLOBALSINDEX) { - if (objstack == 0) { /* no object, only a function */ - lua_replace(L, top + 1); - lua_pop(L, lua_gettop(L) - top - 1); - } else if (objstack == 1) { /* just two values, swap them */ - lua_insert(L, -2); - lua_pop(L, lua_gettop(L) - top - 2); - } else { /* long path */ - lua_insert(L, top + 1); - lua_insert(L, top + 2); - lua_pop(L, objstack - 1); - objstack = 1; - } - } - return 1 + objstack; + return lua_gettop(L) - top; + +no_such_proc: + diag_set(ClientError, ER_NO_SUCH_PROC, name_end - name, name); + return -1; } /** diff --git a/test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua b/test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua new file mode 100644 index 0000000000..8275d480e8 --- /dev/null +++ b/test/box-luatest/gh_8604_support_square_brackets_in_procedure_resolution_for_lua_calls_test.lua @@ -0,0 +1,108 @@ +local t = require('luatest') + +local g = t.group() +g.before_all(function() + local netbox = require('net.box') + + local a = { + b = { + c = function() return 'c' end, + [555] = function() return 555 end + }, + [777] = { + d = { + [444] = function() return 444 end, + e = function() return 'e' end + }, + [666] = function() return 666 end + }, + [555] = function() return 555 end, + [-1] = function() return -1 end, + [333] = netbox.self, + f = function() return 'f' end, + g = netbox.self + } + rawset(_G, 'a', a) +end) + +g.after_all(function() + rawset(_G, 'a', nil) +end) + +-- Checks that procedure resolution for Lua calls works correctly. +g.test_procedure_resolution = function() + local netbox = require('net.box') + local function test(proc) + t.assert_equals(netbox.self:call(proc), + netbox.self:eval('return ' .. proc .. '()')) + end + + test('a.b.c') + test('a.b.c') + test('a.b["c"]') + test('a.b[\'c\']') + test('a.b[555]') + test('a[777].d[444]') + test('a[777].d.e') + test('a[777][666]') + test('a[555]') + test('a[555.]') + test('a[-1]') + test('a[333]:ping') + test('a.f') + test('a.g:ping') +end + +-- Checks that error detection in procedure resolution for Lua calls works +-- correctly. +g.test_procedure_resolution_errors = function() + local netbox = require('net.box') + local function test(proc) + t.assert_error(function() netbox.self:call(proc) end) + end + + test('') + test('.') + test(':') + test('[') + test(']') + test('[]') + test('a.') + test('l:') + test('a.b.') + test('a[b]') + test('a[[]') + test('a[[777]') + test('a["b]') + test('a["b\']') + test('a[\'b]') + test('a[\'b"]') + test('a[\'\']') + test('a[""]') + test('a[\'\']') + test('a["b""]') + test('a["b"\']') + test('a[\'b"\']') + test('a["b\'"]') + test('a[333]:') + test('a[333]:ping:') + test('a:[333]:ping:') + test('a:[333]:') + test('a[555].') + test('a[555].') + test('a[777].[666]') + test('a[777]d[444]') + test('a[777].d.[444]') + test('a[777][666]e') + test('a[555') + test('a[555]..') + test('a[555]..') + test('a[777]..[666]') + test('a[777].][666]') + test('a]555[') + test('a]555]') + test('a]]') + test('a[[555]') + test('a[[555]]') + test('a.b[c]') +end -- GitLab