feat(f_msgpackparse): support parsing from Blob

Note that it is not possible for msgpack_unpack_next() and
msgpack_unpacker_next() to return MSGPACK_UNPACK_EXTRA_BYTES, so it
should be fine to abort() on that.

Lua 5.1 doesn't support string hex escapes (\xXX) like VimL does (though
LuaJIT does), so convert them to decimal escapes (\DDD) in tests.
This commit is contained in:
Sean Dewar 2021-07-29 17:46:45 +01:00
parent ddaa0cc9be
commit e53b71627f
No known key found for this signature in database
GPG Key ID: 08CC2C83AD41B581
3 changed files with 137 additions and 73 deletions

View File

@ -2535,7 +2535,7 @@ mkdir({name} [, {path} [, {prot}]])
Number create directory {name} Number create directory {name}
mode([expr]) String current editing mode mode([expr]) String current editing mode
msgpackdump({list} [, {type}]) List/Blob dump objects to msgpack msgpackdump({list} [, {type}]) List/Blob dump objects to msgpack
msgpackparse({list}) List parse msgpack to a list of objects msgpackparse({data}) List parse msgpack to a list of objects
nextnonblank({lnum}) Number line nr of non-blank line >= {lnum} nextnonblank({lnum}) Number line nr of non-blank line >= {lnum}
nr2char({expr}[, {utf8}]) String single char with ASCII/UTF8 value {expr} nr2char({expr}[, {utf8}]) String single char with ASCII/UTF8 value {expr}
nvim_...({args}...) any call nvim |api| functions nvim_...({args}...) any call nvim |api| functions
@ -6843,8 +6843,9 @@ msgpackdump({list} [, {type}]) *msgpackdump()*
4. Other strings and |Blob|s are always dumped as BIN strings. 4. Other strings and |Blob|s are always dumped as BIN strings.
5. Points 3. and 4. do not apply to |msgpack-special-dict|s. 5. Points 3. and 4. do not apply to |msgpack-special-dict|s.
msgpackparse({list}) *msgpackparse()* msgpackparse({data}) *msgpackparse()*
Convert a |readfile()|-style list to a list of VimL objects. Convert a |readfile()|-style list or a |Blob| to a list of
VimL objects.
Example: > Example: >
let fname = expand('~/.config/nvim/shada/main.shada') let fname = expand('~/.config/nvim/shada/main.shada')
let mpack = readfile(fname, 'b') let mpack = readfile(fname, 'b')

View File

@ -6530,16 +6530,43 @@ static void f_msgpackdump(typval_T *argvars, typval_T *rettv, FunPtr fptr)
msgpack_packer_free(packer); msgpack_packer_free(packer);
} }
/// "msgpackparse" function static int msgpackparse_convert_item(const msgpack_object data,
static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr) const msgpack_unpack_return result,
list_T *const ret_list,
const bool fail_if_incomplete)
FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_ALL
{ {
if (argvars[0].v_type != VAR_LIST) { switch (result) {
EMSG2(_(e_listarg), "msgpackparse()"); case MSGPACK_UNPACK_PARSE_ERROR:
return; EMSG2(_(e_invarg2), "Failed to parse msgpack string");
return FAIL;
case MSGPACK_UNPACK_NOMEM_ERROR:
EMSG(_(e_outofmem));
return FAIL;
case MSGPACK_UNPACK_CONTINUE:
if (fail_if_incomplete) {
EMSG2(_(e_invarg2), "Incomplete msgpack string");
return FAIL;
}
return NOTDONE;
case MSGPACK_UNPACK_SUCCESS: {
typval_T tv = { .v_type = VAR_UNKNOWN };
if (msgpack_to_vim(data, &tv) == FAIL) {
EMSG2(_(e_invarg2), "Failed to convert msgpack string");
return FAIL;
}
tv_list_append_owned_tv(ret_list, tv);
return OK;
}
default:
abort();
} }
list_T *const ret_list = tv_list_alloc_ret(rettv, kListLenMayKnow); }
const list_T *const list = argvars[0].vval.v_list;
static void msgpackparse_unpack_list(const list_T *const list,
list_T *const ret_list)
FUNC_ATTR_NONNULL_ARG(2)
{
if (tv_list_len(list) == 0) { if (tv_list_len(list) == 0) {
return; return;
} }
@ -6558,43 +6585,28 @@ static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr)
do { do {
if (!msgpack_unpacker_reserve_buffer(unpacker, IOSIZE)) { if (!msgpack_unpacker_reserve_buffer(unpacker, IOSIZE)) {
EMSG(_(e_outofmem)); EMSG(_(e_outofmem));
goto f_msgpackparse_exit; goto end;
} }
size_t read_bytes; size_t read_bytes;
const int rlret = encode_read_from_list( const int rlret = encode_read_from_list(
&lrstate, msgpack_unpacker_buffer(unpacker), IOSIZE, &read_bytes); &lrstate, msgpack_unpacker_buffer(unpacker), IOSIZE, &read_bytes);
if (rlret == FAIL) { if (rlret == FAIL) {
EMSG2(_(e_invarg2), "List item is not a string"); EMSG2(_(e_invarg2), "List item is not a string");
goto f_msgpackparse_exit; goto end;
} }
msgpack_unpacker_buffer_consumed(unpacker, read_bytes); msgpack_unpacker_buffer_consumed(unpacker, read_bytes);
if (read_bytes == 0) { if (read_bytes == 0) {
break; break;
} }
while (unpacker->off < unpacker->used) { while (unpacker->off < unpacker->used) {
const msgpack_unpack_return result = msgpack_unpacker_next(unpacker, const msgpack_unpack_return result
&unpacked); = msgpack_unpacker_next(unpacker, &unpacked);
if (result == MSGPACK_UNPACK_PARSE_ERROR) { const int conv_result = msgpackparse_convert_item(unpacked.data, result,
EMSG2(_(e_invarg2), "Failed to parse msgpack string"); ret_list, rlret == OK);
goto f_msgpackparse_exit; if (conv_result == NOTDONE) {
}
if (result == MSGPACK_UNPACK_NOMEM_ERROR) {
EMSG(_(e_outofmem));
goto f_msgpackparse_exit;
}
if (result == MSGPACK_UNPACK_SUCCESS) {
typval_T tv = { .v_type = VAR_UNKNOWN };
if (msgpack_to_vim(unpacked.data, &tv) == FAIL) {
EMSG2(_(e_invarg2), "Failed to convert msgpack string");
goto f_msgpackparse_exit;
}
tv_list_append_owned_tv(ret_list, tv);
}
if (result == MSGPACK_UNPACK_CONTINUE) {
if (rlret == OK) {
EMSG2(_(e_invarg2), "Incomplete msgpack string");
}
break; break;
} else if (conv_result == FAIL) {
goto end;
} }
} }
if (rlret == OK) { if (rlret == OK) {
@ -6602,10 +6614,47 @@ static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr)
} }
} while (true); } while (true);
f_msgpackparse_exit: end:
msgpack_unpacked_destroy(&unpacked);
msgpack_unpacker_free(unpacker); msgpack_unpacker_free(unpacker);
return; msgpack_unpacked_destroy(&unpacked);
}
static void msgpackparse_unpack_blob(const blob_T *const blob,
list_T *const ret_list)
FUNC_ATTR_NONNULL_ARG(2)
{
const int len = tv_blob_len(blob);
if (len == 0) {
return;
}
msgpack_unpacked unpacked;
msgpack_unpacked_init(&unpacked);
for (size_t offset = 0; offset < (size_t)len;) {
const msgpack_unpack_return result
= msgpack_unpack_next(&unpacked, blob->bv_ga.ga_data, len, &offset);
if (msgpackparse_convert_item(unpacked.data, result, ret_list, true)
!= OK) {
break;
}
}
msgpack_unpacked_destroy(&unpacked);
}
/// "msgpackparse" function
static void f_msgpackparse(typval_T *argvars, typval_T *rettv, FunPtr fptr)
FUNC_ATTR_NONNULL_ALL
{
if (argvars[0].v_type != VAR_LIST && argvars[0].v_type != VAR_BLOB) {
EMSG2(_(e_listblobarg), "msgpackparse()");
return;
}
list_T *const ret_list = tv_list_alloc_ret(rettv, kListLenMayKnow);
if (argvars[0].v_type == VAR_LIST) {
msgpackparse_unpack_list(argvars[0].vval.v_list, ret_list);
} else {
msgpackparse_unpack_blob(argvars[0].vval.v_blob, ret_list);
}
} }
/* /*

View File

@ -1,5 +1,6 @@
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local clear = helpers.clear local clear = helpers.clear
local funcs = helpers.funcs
local eval, eq = helpers.eval, helpers.eq local eval, eq = helpers.eval, helpers.eq
local command = helpers.command local command = helpers.command
local nvim = helpers.nvim local nvim = helpers.nvim
@ -12,6 +13,7 @@ describe('msgpack*() functions', function()
it(msg, function() it(msg, function()
nvim('set_var', 'obj', obj) nvim('set_var', 'obj', obj)
eq(obj, eval('msgpackparse(msgpackdump(g:obj))')) eq(obj, eval('msgpackparse(msgpackdump(g:obj))'))
eq(obj, eval('msgpackparse(msgpackdump(g:obj, "B"))'))
end) end)
end end
@ -390,56 +392,61 @@ describe('msgpack*() functions', function()
end) end)
end) end)
local blobstr = function(list)
local l = {}
for i,v in ipairs(list) do
l[i] = v:gsub('\n', '\000')
end
return table.concat(l, '\n')
end
-- Test msgpackparse() with a readfile()-style list and a blob argument
local parse_eq = function(expect, list_arg)
local blob_expr = '0z' .. blobstr(list_arg):gsub('(.)', function(c)
return ('%.2x'):format(c:byte())
end)
eq(expect, funcs.msgpackparse(list_arg))
command('let g:parsed = msgpackparse(' .. blob_expr .. ')')
eq(expect, eval('g:parsed'))
end
describe('msgpackparse() function', function() describe('msgpackparse() function', function()
before_each(clear) before_each(clear)
it('restores nil as v:null', function() it('restores nil as v:null', function()
command('let dumped = ["\\xC0"]') parse_eq(eval('[v:null]'), {'\192'})
command('let parsed = msgpackparse(dumped)')
eq('[v:null]', eval('string(parsed)'))
end) end)
it('restores boolean false as v:false', function() it('restores boolean false as v:false', function()
command('let dumped = ["\\xC2"]') parse_eq({false}, {'\194'})
command('let parsed = msgpackparse(dumped)')
eq({false}, eval('parsed'))
end) end)
it('restores boolean true as v:true', function() it('restores boolean true as v:true', function()
command('let dumped = ["\\xC3"]') parse_eq({true}, {'\195'})
command('let parsed = msgpackparse(dumped)')
eq({true}, eval('parsed'))
end) end)
it('restores FIXSTR as special dict', function() it('restores FIXSTR as special dict', function()
command('let dumped = ["\\xa2ab"]') parse_eq({{_TYPE={}, _VAL={'ab'}}}, {'\162ab'})
command('let parsed = msgpackparse(dumped)')
eq({{_TYPE={}, _VAL={'ab'}}}, eval('parsed'))
eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.string')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.string'))
end) end)
it('restores BIN 8 as string', function() it('restores BIN 8 as string', function()
command('let dumped = ["\\xC4\\x02ab"]') parse_eq({'ab'}, {'\196\002ab'})
eq({'ab'}, eval('msgpackparse(dumped)'))
end) end)
it('restores FIXEXT1 as special dictionary', function() it('restores FIXEXT1 as special dictionary', function()
command('let dumped = ["\\xD4\\x10", ""]') parse_eq({{_TYPE={}, _VAL={0x10, {"", ""}}}}, {'\212\016', ''})
command('let parsed = msgpackparse(dumped)')
eq({{_TYPE={}, _VAL={0x10, {"", ""}}}}, eval('parsed'))
eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.ext')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.ext'))
end) end)
it('restores MAP with BIN key as special dictionary', function() it('restores MAP with BIN key as special dictionary', function()
command('let dumped = ["\\x81\\xC4\\x01a\\xC4\\n"]') parse_eq({{_TYPE={}, _VAL={{'a', ''}}}}, {'\129\196\001a\196\n'})
command('let parsed = msgpackparse(dumped)')
eq({{_TYPE={}, _VAL={{'a', ''}}}}, eval('parsed'))
eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map'))
end) end)
it('restores MAP with duplicate STR keys as special dictionary', function() it('restores MAP with duplicate STR keys as special dictionary', function()
command('let dumped = ["\\x82\\xA1a\\xC4\\n\\xA1a\\xC4\\n"]') command('let dumped = ["\\x82\\xA1a\\xC4\\n\\xA1a\\xC4\\n"]')
-- FIXME Internal error bug -- FIXME Internal error bug, can't use parse_eq() here
command('silent! let parsed = msgpackparse(dumped)') command('silent! let parsed = msgpackparse(dumped)')
eq({{_TYPE={}, _VAL={ {{_TYPE={}, _VAL={'a'}}, ''}, eq({{_TYPE={}, _VAL={ {{_TYPE={}, _VAL={'a'}}, ''},
{{_TYPE={}, _VAL={'a'}}, ''}}} }, eval('parsed')) {{_TYPE={}, _VAL={'a'}}, ''}}} }, eval('parsed'))
@ -449,9 +456,7 @@ describe('msgpackparse() function', function()
end) end)
it('restores MAP with MAP key as special dictionary', function() it('restores MAP with MAP key as special dictionary', function()
command('let dumped = ["\\x81\\x80\\xC4\\n"]') parse_eq({{_TYPE={}, _VAL={{{}, ''}}}}, {'\129\128\196\n'})
command('let parsed = msgpackparse(dumped)')
eq({{_TYPE={}, _VAL={{{}, ''}}}}, eval('parsed'))
eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map'))
end) end)
@ -476,48 +481,57 @@ describe('msgpackparse() function', function()
end) end)
it('fails to parse a string', function() it('fails to parse a string', function()
eq('Vim(call):E686: Argument of msgpackparse() must be a List', eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob',
exc_exec('call msgpackparse("abcdefghijklmnopqrstuvwxyz")')) exc_exec('call msgpackparse("abcdefghijklmnopqrstuvwxyz")'))
end) end)
it('fails to parse a number', function() it('fails to parse a number', function()
eq('Vim(call):E686: Argument of msgpackparse() must be a List', eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob',
exc_exec('call msgpackparse(127)')) exc_exec('call msgpackparse(127)'))
end) end)
it('fails to parse a dictionary', function() it('fails to parse a dictionary', function()
eq('Vim(call):E686: Argument of msgpackparse() must be a List', eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob',
exc_exec('call msgpackparse({})')) exc_exec('call msgpackparse({})'))
end) end)
it('fails to parse a funcref', function() it('fails to parse a funcref', function()
eq('Vim(call):E686: Argument of msgpackparse() must be a List', eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob',
exc_exec('call msgpackparse(function("tr"))')) exc_exec('call msgpackparse(function("tr"))'))
end) end)
it('fails to parse a partial', function() it('fails to parse a partial', function()
command('function T() dict\nendfunction') command('function T() dict\nendfunction')
eq('Vim(call):E686: Argument of msgpackparse() must be a List', eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob',
exc_exec('call msgpackparse(function("T", [1, 2], {}))')) exc_exec('call msgpackparse(function("T", [1, 2], {}))'))
end) end)
it('fails to parse a float', function() it('fails to parse a float', function()
eq('Vim(call):E686: Argument of msgpackparse() must be a List', eq('Vim(call):E899: Argument of msgpackparse() must be a List or Blob',
exc_exec('call msgpackparse(0.0)')) exc_exec('call msgpackparse(0.0)'))
end) end)
it('fails on incomplete msgpack string', function()
local expected = 'Vim(call):E475: Invalid argument: Incomplete msgpack string'
eq(expected, exc_exec([[call msgpackparse(["\xc4"])]]))
eq(expected, exc_exec([[call msgpackparse(["\xca", "\x02\x03"])]]))
eq(expected, exc_exec('call msgpackparse(0zc4)'))
eq(expected, exc_exec('call msgpackparse(0zca0a0203)'))
end)
it('fails when unable to parse msgpack string', function()
local expected = 'Vim(call):E475: Invalid argument: Failed to parse msgpack string'
eq(expected, exc_exec([[call msgpackparse(["\xc1"])]]))
eq(expected, exc_exec('call msgpackparse(0zc1)'))
end)
end) end)
describe('msgpackdump() function', function() describe('msgpackdump() function', function()
before_each(clear) before_each(clear)
local dump_eq = function(exp_list, arg_expr) local dump_eq = function(exp_list, arg_expr)
local l = {}
for i,v in ipairs(exp_list) do
l[i] = v:gsub('\n', '\000')
end
local exp_blobstr = table.concat(l, '\n')
eq(exp_list, eval('msgpackdump(' .. arg_expr .. ')')) eq(exp_list, eval('msgpackdump(' .. arg_expr .. ')'))
eq(exp_blobstr, eval('msgpackdump(' .. arg_expr .. ', "B")')) eq(blobstr(exp_list), eval('msgpackdump(' .. arg_expr .. ', "B")'))
end end
it('dumps string as BIN 8', function() it('dumps string as BIN 8', function()