diff options
| -rw-r--r-- | src/nvim/api/private/helpers.c | 318 | ||||
| -rw-r--r-- | src/nvim/api/private/helpers.h | 11 | ||||
| -rw-r--r-- | src/nvim/eval/encode.c | 7 | ||||
| -rw-r--r-- | src/nvim/eval/typval_encode.h | 2 | ||||
| -rw-r--r-- | test/functional/api/server_notifications_spec.lua | 13 | 
5 files changed, 214 insertions, 137 deletions
| diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index db3e499427..0d527b0629 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -17,6 +17,13 @@  #include "nvim/map.h"  #include "nvim/option.h"  #include "nvim/option_defs.h" +#include "nvim/eval/typval_encode.h" +#include "nvim/lib/kvec.h" + +/// Helper structure for vim_to_object +typedef struct { +  kvec_t(Object) stack;  ///< Object stack. +} EncodedData;  #ifdef INCLUDE_GENERATED_DECLARATIONS  # include "api/private/helpers.c.generated.h" @@ -310,6 +317,179 @@ void set_option_to(void *to, int type, String name, Object value, Error *err)    }  } +#define TYPVAL_ENCODE_ALLOW_SPECIALS false + +#define TYPVAL_ENCODE_CONV_NIL() \ +    kv_push(edata->stack, NIL) + +#define TYPVAL_ENCODE_CONV_BOOL(num) \ +    kv_push(edata->stack, BOOLEAN_OBJ((Boolean) (num))) + +#define TYPVAL_ENCODE_CONV_NUMBER(num) \ +    kv_push(edata->stack, INTEGER_OBJ((Integer) (num))) + +#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER TYPVAL_ENCODE_CONV_NUMBER + +#define TYPVAL_ENCODE_CONV_FLOAT(flt) \ +    kv_push(edata->stack, FLOATING_OBJ((Float) (flt))) + +#define TYPVAL_ENCODE_CONV_STRING(str, len) \ +    do { \ +      const size_t len_ = (size_t) (len); \ +      const char *const str_ = (const char *) (str); \ +      assert(len_ == 0 || str_ != NULL); \ +      kv_push(edata->stack, STRING_OBJ(((String) { \ +        .data = xmemdupz((len_?str_:""), len_), \ +        .size = len_ \ +      }))); \ +    } while (0) + +#define TYPVAL_ENCODE_CONV_STR_STRING TYPVAL_ENCODE_CONV_STRING + +#define TYPVAL_ENCODE_CONV_EXT_STRING(str, len, type) \ +    TYPVAL_ENCODE_CONV_NIL() + +#define TYPVAL_ENCODE_CONV_FUNC(fun) \ +    TYPVAL_ENCODE_CONV_NIL() + +#define TYPVAL_ENCODE_CONV_EMPTY_LIST() \ +    kv_push(edata->stack, ARRAY_OBJ(((Array) { .capacity = 0, .size = 0 }))) + +#define TYPVAL_ENCODE_CONV_EMPTY_DICT() \ +    kv_push(edata->stack, \ +            DICTIONARY_OBJ(((Dictionary) { .capacity = 0, .size = 0 }))) + +static inline void typval_encode_list_start(EncodedData *const edata, +                                            const size_t len) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  const Object obj = OBJECT_INIT; +  kv_push(edata->stack, ARRAY_OBJ(((Array) { +    .capacity = len, +    .size = 0, +    .items = xmalloc(len * sizeof(*obj.data.array.items)), +  }))); +} + +#define TYPVAL_ENCODE_CONV_LIST_START(len) \ +    typval_encode_list_start(edata, (size_t) (len)) + +static inline void typval_encode_between_list_items(EncodedData *const edata) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  Object item = kv_pop(edata->stack); +  Object *const list = &kv_last(edata->stack); +  assert(list->type == kObjectTypeArray); +  assert(list->data.array.size < list->data.array.capacity); +  list->data.array.items[list->data.array.size++] = item; +} + +#define TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS() \ +    typval_encode_between_list_items(edata) + +static inline void typval_encode_list_end(EncodedData *const edata) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  typval_encode_between_list_items(edata); +#ifndef NDEBUG +  const Object *const list = &kv_last(edata->stack); +  assert(list->data.array.size == list->data.array.capacity); +#endif +} + +#define TYPVAL_ENCODE_CONV_LIST_END() \ +    typval_encode_list_end(edata) + +static inline void typval_encode_dict_start(EncodedData *const edata, +                                            const size_t len) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  const Object obj = OBJECT_INIT; +  kv_push(edata->stack, DICTIONARY_OBJ(((Dictionary) { +    .capacity = len, +    .size = 0, +    .items = xmalloc(len * sizeof(*obj.data.dictionary.items)), +  }))); +} + +#define TYPVAL_ENCODE_CONV_DICT_START(len) \ +    typval_encode_dict_start(edata, (size_t) (len)) + +#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, kv_pair) + +static inline void typval_encode_after_key(EncodedData *const edata) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  Object key = kv_pop(edata->stack); +  Object *const dict = &kv_last(edata->stack); +  assert(dict->type == kObjectTypeDictionary); +  assert(dict->data.dictionary.size < dict->data.dictionary.capacity); +  if (key.type == kObjectTypeString) { +    dict->data.dictionary.items[dict->data.dictionary.size].key +        = key.data.string; +  } else { +    api_free_object(key); +    dict->data.dictionary.items[dict->data.dictionary.size].key +        = STATIC_CSTR_TO_STRING("__INVALID_KEY__"); +  } +} + +#define TYPVAL_ENCODE_CONV_DICT_AFTER_KEY() \ +    typval_encode_after_key(edata) + +static inline void typval_encode_between_dict_items(EncodedData *const edata) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  Object val = kv_pop(edata->stack); +  Object *const dict = &kv_last(edata->stack); +  assert(dict->type == kObjectTypeDictionary); +  assert(dict->data.dictionary.size < dict->data.dictionary.capacity); +  dict->data.dictionary.items[dict->data.dictionary.size++].value = val; +} + +#define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS() \ +    typval_encode_between_dict_items(edata) + +static inline void typval_encode_dict_end(EncodedData *const edata) +  FUNC_ATTR_ALWAYS_INLINE FUNC_ATTR_NONNULL_ALL +{ +  typval_encode_between_dict_items(edata); +#ifndef NDEBUG +  const Object *const dict = &kv_last(edata->stack); +  assert(dict->data.dictionary.size == dict->data.dictionary.capacity); +#endif +} + +#define TYPVAL_ENCODE_CONV_DICT_END() \ +    typval_encode_dict_end(edata) + +#define TYPVAL_ENCODE_CONV_RECURSE(val, conv_type) \ +    TYPVAL_ENCODE_CONV_NIL() + +TYPVAL_ENCODE_DEFINE_CONV_FUNCTIONS(static, object, EncodedData *const, edata) + +#undef TYPVAL_ENCODE_CONV_STRING +#undef TYPVAL_ENCODE_CONV_STR_STRING +#undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_NUMBER +#undef TYPVAL_ENCODE_CONV_FLOAT +#undef TYPVAL_ENCODE_CONV_FUNC +#undef TYPVAL_ENCODE_CONV_EMPTY_LIST +#undef TYPVAL_ENCODE_CONV_LIST_START +#undef TYPVAL_ENCODE_CONV_EMPTY_DICT +#undef TYPVAL_ENCODE_CONV_NIL +#undef TYPVAL_ENCODE_CONV_BOOL +#undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER +#undef TYPVAL_ENCODE_CONV_DICT_START +#undef TYPVAL_ENCODE_CONV_DICT_END +#undef TYPVAL_ENCODE_CONV_DICT_AFTER_KEY +#undef TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS +#undef TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK +#undef TYPVAL_ENCODE_CONV_LIST_END +#undef TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS +#undef TYPVAL_ENCODE_CONV_RECURSE +#undef TYPVAL_ENCODE_ALLOW_SPECIALS +  /// Convert a vim object to an `Object` instance, recursively expanding  /// Arrays/Dictionaries.  /// @@ -317,13 +497,12 @@ void set_option_to(void *to, int type, String name, Object value, Error *err)  /// @return The converted value  Object vim_to_object(typval_T *obj)  { -  Object rv; -  // We use a lookup table to break out of cyclic references -  PMap(ptr_t) *lookup = pmap_new(ptr_t)(); -  rv = vim_to_object_rec(obj, lookup); -  // Free the table -  pmap_free(ptr_t)(lookup); -  return rv; +  EncodedData edata = { .stack = KV_INITIAL_VALUE }; +  encode_vim_to_object(&edata, obj, "vim_to_object argument"); +  Object ret = kv_A(edata.stack, 0); +  assert(kv_size(edata.stack) == 1); +  kv_destroy(edata.stack); +  return ret;  }  buf_T *find_buffer_by_handle(Buffer buffer, Error *err) @@ -633,131 +812,6 @@ Object copy_object(Object obj)    }  } -/// Recursion helper for the `vim_to_object`. This uses a pointer table -/// to avoid infinite recursion due to cyclic references -/// -/// @param obj The source object -/// @param lookup Lookup table containing pointers to all processed objects -/// @return The converted value -static Object vim_to_object_rec(typval_T *obj, PMap(ptr_t) *lookup) -{ -  Object rv = OBJECT_INIT; - -  if (obj->v_type == VAR_LIST || obj->v_type == VAR_DICT) { -    // Container object, add it to the lookup table -    if (pmap_has(ptr_t)(lookup, obj)) { -      // It's already present, meaning we alredy processed it so just return -      // nil instead. -      return rv; -    } -    pmap_put(ptr_t)(lookup, obj, NULL); -  } - -  switch (obj->v_type) { -    case VAR_SPECIAL: -      switch (obj->vval.v_special) { -        case kSpecialVarTrue: -        case kSpecialVarFalse: { -          rv.type = kObjectTypeBoolean; -          rv.data.boolean = (obj->vval.v_special == kSpecialVarTrue); -          break; -        } -        case kSpecialVarNull: { -          rv.type = kObjectTypeNil; -          break; -        } -      } -      break; - -    case VAR_STRING: -      rv.type = kObjectTypeString; -      rv.data.string = cstr_to_string((char *) obj->vval.v_string); -      break; - -    case VAR_NUMBER: -      rv.type = kObjectTypeInteger; -      rv.data.integer = obj->vval.v_number; -      break; - -    case VAR_FLOAT: -      rv.type = kObjectTypeFloat; -      rv.data.floating = obj->vval.v_float; -      break; - -    case VAR_LIST: -      { -        list_T *list = obj->vval.v_list; -        listitem_T *item; - -        if (list != NULL) { -          rv.type = kObjectTypeArray; -          assert(list->lv_len >= 0); -          rv.data.array.size = (size_t)list->lv_len; -          rv.data.array.items = xmalloc(rv.data.array.size * sizeof(Object)); - -          uint32_t i = 0; -          for (item = list->lv_first; item != NULL; item = item->li_next) { -            rv.data.array.items[i] = vim_to_object_rec(&item->li_tv, lookup); -            i++; -          } -        } -      } -      break; - -    case VAR_DICT: -      { -        dict_T *dict = obj->vval.v_dict; -        hashtab_T *ht; -        uint64_t todo; -        hashitem_T *hi; -        dictitem_T *di; - -        if (dict != NULL) { -          ht = &obj->vval.v_dict->dv_hashtab; -          todo = ht->ht_used; -          rv.type = kObjectTypeDictionary; - -          // Count items -          rv.data.dictionary.size = 0; -          for (hi = ht->ht_array; todo > 0; ++hi) { -            if (!HASHITEM_EMPTY(hi)) { -              todo--; -              rv.data.dictionary.size++; -            } -          } - -          rv.data.dictionary.items = -            xmalloc(rv.data.dictionary.size * sizeof(KeyValuePair)); -          todo = ht->ht_used; -          uint32_t i = 0; - -          // Convert all -          for (hi = ht->ht_array; todo > 0; ++hi) { -            if (!HASHITEM_EMPTY(hi)) { -              di = dict_lookup(hi); -              // Convert key -              rv.data.dictionary.items[i].key = -                cstr_to_string((char *) hi->hi_key); -              // Convert value -              rv.data.dictionary.items[i].value = -                vim_to_object_rec(&di->di_tv, lookup); -              todo--; -              i++; -            } -          } -        } -      } -      break; - -    case VAR_UNKNOWN: -    case VAR_FUNC: -      break; -  } - -  return rv; -} - -  static void set_option_value_for(char *key,                                   int numval,                                   char *stringval, diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index 731f186ecc..a946e35149 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -27,6 +27,10 @@      .type = kObjectTypeInteger, \      .data.integer = i }) +#define FLOATING_OBJ(f) ((Object) { \ +    .type = kObjectTypeFloat, \ +    .data.floating = f }) +  #define STRING_OBJ(s) ((Object) { \      .type = kObjectTypeString, \      .data.string = s }) @@ -61,6 +65,13 @@  #define STATIC_CSTR_AS_STRING(s) ((String) {.data = s, .size = sizeof(s) - 1}) +/// Create a new String instance, putting data in allocated memory +/// +/// @param[in]  s  String to work with. Must be a string literal. +#define STATIC_CSTR_TO_STRING(s) ((String){ \ +    .data = xmemdupz(s, sizeof(s) - 1), \ +    .size = sizeof(s) - 1 }) +  // Helpers used by the generated msgpack-rpc api wrappers  #define api_init_boolean  #define api_init_integer diff --git a/src/nvim/eval/encode.c b/src/nvim/eval/encode.c index 54daf7557e..771655fee5 100644 --- a/src/nvim/eval/encode.c +++ b/src/nvim/eval/encode.c @@ -379,7 +379,6 @@ int encode_read_from_list(ListReaderState *const state, char *const buf,        } \        vim_snprintf(ebuf, ARRAY_SIZE(ebuf), "{E724@%zu}", backref); \        ga_concat(gap, &ebuf[0]); \ -      return OK; \      } while (0)  #define TYPVAL_ENCODE_ALLOW_SPECIALS false @@ -426,7 +425,6 @@ TYPVAL_ENCODE_DEFINE_CONV_FUNCTIONS(, echo, garray_T *const, gap)          EMSG(_("E724: unable to correctly dump variable " \                 "with self-referencing container")); \        } \ -      return OK; \      } while (0)  #undef TYPVAL_ENCODE_ALLOW_SPECIALS @@ -662,9 +660,8 @@ static inline int convert_to_json_string(garray_T *const gap,  /// Check whether given key can be used in json_encode()  ///  /// @param[in]  tv  Key to check. -static inline bool check_json_key(const typval_T *const tv) +bool encode_check_json_key(const typval_T *const tv)    FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE -  FUNC_ATTR_ALWAYS_INLINE  {    if (tv->v_type == VAR_STRING) {      return true; @@ -701,7 +698,7 @@ static inline bool check_json_key(const typval_T *const tv)  #undef TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK  #define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, kv_pair) \      do { \ -      if (!check_json_key(&kv_pair->lv_first->li_tv)) { \ +      if (!encode_check_json_key(&kv_pair->lv_first->li_tv)) { \          EMSG(_("E474: Invalid key in special dictionary")); \          goto label; \        } \ diff --git a/src/nvim/eval/typval_encode.h b/src/nvim/eval/typval_encode.h index f70a6c9e94..c2e8403475 100644 --- a/src/nvim/eval/typval_encode.h +++ b/src/nvim/eval/typval_encode.h @@ -184,10 +184,12 @@ typedef kvec_t(MPConvStackVal) MPConvStack;  /// @param  copyID_attr  Name of the container attribute that holds copyID.  ///                      After checking whether value of this attribute is  ///                      copyID (variable) it is set to copyID. +/// @param  conv_type  Type of the conversion, @see MPConvStackValType.  #define _TYPVAL_ENCODE_CHECK_SELF_REFERENCE(val, copyID_attr, conv_type) \      do { \        if ((val)->copyID_attr == copyID) { \          TYPVAL_ENCODE_CONV_RECURSE((val), conv_type); \ +        return OK; \        } \        (val)->copyID_attr = copyID; \      } while (0) diff --git a/test/functional/api/server_notifications_spec.lua b/test/functional/api/server_notifications_spec.lua index d68ce411c0..110bc6cbf1 100644 --- a/test/functional/api/server_notifications_spec.lua +++ b/test/functional/api/server_notifications_spec.lua @@ -3,6 +3,7 @@ local helpers = require('test.functional.helpers')(after_each)  local eq, clear, eval, execute, nvim, next_message =    helpers.eq, helpers.clear, helpers.eval, helpers.execute, helpers.nvim,    helpers.next_message +local meths = helpers.meths  describe('notify', function()    local channel @@ -36,5 +37,17 @@ describe('notify', function()        eval('rpcnotify(0, "event1", 13, 14, 15)')        eq({'notification', 'event1', {13, 14, 15}}, next_message())      end) + +    it('does not crash for deeply nested variable', function() +      meths.set_var('l', {}) +      local nest_level = 100000 +      meths.command(('call map(range(%u), "extend(g:, {\'l\': [g:l]})")'):format(nest_level)) +      local ret = {} +      for i = 1, nest_level do +        ret = {ret} +      end +      eval('rpcnotify('..channel..', "event", g:l)') +      -- eq({'notification', 'event', ret}, next_message()) +    end)    end)  end) | 
