diff options
28 files changed, 2515 insertions, 637 deletions
diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index e794a8c5b9..cf84f8c6a4 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -27,12 +27,15 @@ if(NOT HAVE_SYS_WAIT_H AND UNIX) endif() check_include_files(sys/utsname.h HAVE_SYS_UTSNAME_H) check_include_files(utime.h HAVE_UTIME_H) +check_include_files(sys/uio.h HAVE_SYS_UIO_H) # Functions check_function_exists(fseeko HAVE_FSEEKO) check_function_exists(getpwent HAVE_GETPWENT) check_function_exists(getpwnam HAVE_GETPWNAM) check_function_exists(getpwuid HAVE_GETPWUID) +check_function_exists(uv_translate_sys_error HAVE_UV_TRANSLATE_SYS_ERROR) +check_function_exists(readv HAVE_READV) if(Iconv_FOUND) set(HAVE_ICONV 1) diff --git a/config/config.h.in b/config/config.h.in index 867278de0d..4c35b3b1cb 100644 --- a/config/config.h.in +++ b/config/config.h.in @@ -30,6 +30,7 @@ #cmakedefine HAVE_PUTENV_S #cmakedefine HAVE_PWD_H #cmakedefine HAVE_READLINK +#cmakedefine HAVE_UV_TRANSLATE_SYS_ERROR // TODO: add proper cmake check // #define HAVE_SELINUX 1 #cmakedefine HAVE_SETENV @@ -48,6 +49,13 @@ #cmakedefine HAVE_WORKING_LIBINTL #cmakedefine UNIX #cmakedefine USE_FNAME_CASE +#cmakedefine HAVE_SYS_UIO_H +#ifdef HAVE_SYS_UIO_H +#cmakedefine HAVE_READV +# ifndef HAVE_READV +# undef HAVE_SYS_UIO_H +# endif +#endif #cmakedefine FEAT_TUI diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 50b1262347..06c0678aca 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -4938,7 +4938,7 @@ A jump table for the options with a short description can be found at |Q_op|. $XDG_CONFIG_DIRS[1]/nvim, $XDG_CONFIG_DIRS[2]/nvim, … - $XDG_DATA_HOME/nvim, + $XDG_DATA_HOME/nvim/site, $XDG_DATA_DIRS[1]/nvim/site, $XDG_DATA_DIRS[2]/nvim/site, … @@ -4946,7 +4946,7 @@ A jump table for the options with a short description can be found at |Q_op|. … $XDG_DATA_DIRS[2]/nvim/site/after, $XDG_DATA_DIRS[1]/nvim/site/after, - $XDG_DATA_HOME/nvim/after, + $XDG_DATA_HOME/nvim/site/after, … $XDG_CONFIG_DIRS[2]/nvim/after, $XDG_CONFIG_DIRS[1]/nvim/after, diff --git a/src/clint.py b/src/clint.py index 80e4c4ac39..efc5f18378 100755 --- a/src/clint.py +++ b/src/clint.py @@ -2515,7 +2515,8 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error): 'after the hash') cast_line = re.sub(r'^# *define +\w+\([^)]*\)', '', line) - match = Search(r'\((?:const )?(?:struct )?[a-zA-Z_]\w*(?: *\*(?:const)?)*\)' + match = Search(r'(?<!\bkvec_t)' + r'\((?:const )?(?:struct )?[a-zA-Z_]\w*(?: *\*(?:const)?)*\)' r' +' r'-?(?:\*+|&)?(?:\w+|\+\+|--|\()', cast_line) if match and line[0] == ' ': diff --git a/src/nvim/api/private/defs.h b/src/nvim/api/private/defs.h index 80a88becf4..5fb95a163f 100644 --- a/src/nvim/api/private/defs.h +++ b/src/nvim/api/private/defs.h @@ -41,6 +41,12 @@ typedef bool Boolean; typedef int64_t Integer; typedef double Float; +/// Maximum value of an Integer +#define API_INTEGER_MAX INT64_MAX + +/// Minimum value of an Integer +#define API_INTEGER_MIN INT64_MIN + typedef struct { char *data; size_t size; diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index db3e499427..c88bf2127a 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.c b/src/nvim/eval.c index aa5cd676af..f5cffbc3e1 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -91,6 +91,7 @@ #include "nvim/os/input.h" #include "nvim/event/loop.h" #include "nvim/lib/queue.h" +#include "nvim/eval/typval_encode.h" #define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */ @@ -18148,45 +18149,147 @@ void free_tv(typval_T *varp) } } -/* - * Free the memory for a variable value and set the value to NULL or 0. - */ +#define TYPVAL_ENCODE_ALLOW_SPECIALS false + +#define TYPVAL_ENCODE_CONV_NIL() \ + do { \ + tv->vval.v_special = kSpecialVarFalse; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_BOOL(ignored) \ + TYPVAL_ENCODE_CONV_NIL() + +#define TYPVAL_ENCODE_CONV_NUMBER(ignored) \ + do { \ + (void)ignored; \ + tv->vval.v_number = 0; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER(ignored) \ + assert(false) + +#define TYPVAL_ENCODE_CONV_FLOAT(ignored) \ + do { \ + tv->vval.v_float = 0; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_STRING(str, ignored) \ + do { \ + xfree(str); \ + tv->vval.v_string = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_STR_STRING(ignored1, ignored2) + +#define TYPVAL_ENCODE_CONV_EXT_STRING(ignored1, ignored2, ignored3) + +#define TYPVAL_ENCODE_CONV_FUNC(fun) \ + do { \ + func_unref(fun); \ + if (fun != empty_string) { \ + xfree(fun); \ + } \ + tv->vval.v_string = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_EMPTY_LIST() \ + do { \ + list_unref(tv->vval.v_list); \ + tv->vval.v_list = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_EMPTY_DICT() \ + do { \ + dict_unref(tv->vval.v_dict); \ + tv->vval.v_dict = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_LIST_START(ignored) \ + do { \ + if (tv->vval.v_list->lv_refcount > 1) { \ + tv->vval.v_list->lv_refcount--; \ + tv->vval.v_list = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + return OK; \ + } \ + } while (0) + +#define TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS() + +#define TYPVAL_ENCODE_CONV_LIST_END() \ + do { \ + typval_T *const cur_tv = cur_mpsv->tv; \ + assert(cur_tv->v_type == VAR_LIST); \ + list_unref(cur_tv->vval.v_list); \ + cur_tv->vval.v_list = NULL; \ + cur_tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_DICT_START(ignored) \ + do { \ + if (tv->vval.v_dict->dv_refcount > 1) { \ + tv->vval.v_dict->dv_refcount--; \ + tv->vval.v_dict = NULL; \ + tv->v_lock = VAR_UNLOCKED; \ + return OK; \ + } \ + } while (0) + +#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(ignored1, ignored2) + +#define TYPVAL_ENCODE_CONV_DICT_AFTER_KEY() + +#define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS() + +#define TYPVAL_ENCODE_CONV_DICT_END() \ + do { \ + typval_T *const cur_tv = cur_mpsv->tv; \ + assert(cur_tv->v_type == VAR_DICT); \ + dict_unref(cur_tv->vval.v_dict); \ + cur_tv->vval.v_dict = NULL; \ + cur_tv->v_lock = VAR_UNLOCKED; \ + } while (0) + +#define TYPVAL_ENCODE_CONV_RECURSE(ignored1, ignored2) + +TYPVAL_ENCODE_DEFINE_CONV_FUNCTIONS(static, nothing, void *, ignored) + +#undef TYPVAL_ENCODE_ALLOW_SPECIALS +#undef TYPVAL_ENCODE_CONV_NIL +#undef TYPVAL_ENCODE_CONV_BOOL +#undef TYPVAL_ENCODE_CONV_NUMBER +#undef TYPVAL_ENCODE_CONV_UNSIGNED_NUMBER +#undef TYPVAL_ENCODE_CONV_FLOAT +#undef TYPVAL_ENCODE_CONV_STRING +#undef TYPVAL_ENCODE_CONV_STR_STRING +#undef TYPVAL_ENCODE_CONV_EXT_STRING +#undef TYPVAL_ENCODE_CONV_FUNC +#undef TYPVAL_ENCODE_CONV_EMPTY_LIST +#undef TYPVAL_ENCODE_CONV_EMPTY_DICT +#undef TYPVAL_ENCODE_CONV_LIST_START +#undef TYPVAL_ENCODE_CONV_LIST_BETWEEN_ITEMS +#undef TYPVAL_ENCODE_CONV_LIST_END +#undef TYPVAL_ENCODE_CONV_DICT_START +#undef TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK +#undef TYPVAL_ENCODE_CONV_DICT_AFTER_KEY +#undef TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS +#undef TYPVAL_ENCODE_CONV_DICT_END +#undef TYPVAL_ENCODE_CONV_RECURSE + +/// Free memory for a variable value and set the value to NULL or 0 +/// +/// @param[in,out] varp Value to free. void clear_tv(typval_T *varp) { - if (varp != NULL) { - switch (varp->v_type) { - case VAR_FUNC: - func_unref(varp->vval.v_string); - if (varp->vval.v_string != empty_string) { - xfree(varp->vval.v_string); - } - varp->vval.v_string = NULL; - break; - case VAR_STRING: - xfree(varp->vval.v_string); - varp->vval.v_string = NULL; - break; - case VAR_LIST: - list_unref(varp->vval.v_list); - varp->vval.v_list = NULL; - break; - case VAR_DICT: - dict_unref(varp->vval.v_dict); - varp->vval.v_dict = NULL; - break; - case VAR_NUMBER: - varp->vval.v_number = 0; - break; - case VAR_FLOAT: - varp->vval.v_float = 0.0; - break; - case VAR_SPECIAL: - varp->vval.v_special = kSpecialVarFalse; - break; - case VAR_UNKNOWN: - break; - } - varp->v_lock = 0; + if (varp != NULL && varp->v_type != VAR_UNKNOWN) { + encode_vim_to_nothing(varp, varp, "clear_tv argument"); } } diff --git a/src/nvim/eval/encode.c b/src/nvim/eval/encode.c index 54daf7557e..670437ceda 100644 --- a/src/nvim/eval/encode.c +++ b/src/nvim/eval/encode.c @@ -344,7 +344,7 @@ int encode_read_from_list(ListReaderState *const state, char *const buf, #define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS() \ ga_concat(gap, ", ") -#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, kv_pair) +#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, key) #define TYPVAL_ENCODE_CONV_LIST_END() \ ga_append(gap, ']') @@ -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; @@ -699,9 +696,9 @@ 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) \ +#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, key) \ do { \ - if (!check_json_key(&kv_pair->lv_first->li_tv)) { \ + if (!encode_check_json_key(&key)) { \ EMSG(_("E474: Invalid key in special dictionary")); \ goto label; \ } \ @@ -876,7 +873,7 @@ char *encode_tv2json(typval_T *tv, size_t *len) #define TYPVAL_ENCODE_CONV_DICT_BETWEEN_ITEMS() -#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, kv_pair) +#define TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK(label, key) #define TYPVAL_ENCODE_CONV_LIST_END() diff --git a/src/nvim/eval/typval_encode.h b/src/nvim/eval/typval_encode.h index f70a6c9e94..98fa7b26c6 100644 --- a/src/nvim/eval/typval_encode.h +++ b/src/nvim/eval/typval_encode.h @@ -104,7 +104,7 @@ /// @brief Macros used to check special dictionary key /// /// @param label Label for goto in case check was not successfull. -/// @param kv_pair List with two elements: key and value. +/// @param key typval_T key to check. /// @def TYPVAL_ENCODE_CONV_DICT_AFTER_KEY /// @brief Macros used after finishing converting dictionary key @@ -154,6 +154,7 @@ typedef enum { /// Structure representing current VimL to messagepack conversion state typedef struct { MPConvStackValType type; ///< Type of the stack entry. + typval_T *tv; ///< Currently converted typval_T. union { struct { dict_T *dict; ///< Currently converted dictionary. @@ -168,13 +169,13 @@ typedef struct { } MPConvStackVal; /// Stack used to convert VimL values to messagepack. -typedef kvec_t(MPConvStackVal) MPConvStack; +typedef kvec_withinit_t(MPConvStackVal, 8) MPConvStack; // Defines for MPConvStack #define _mp_size kv_size -#define _mp_init kv_init -#define _mp_destroy kv_destroy -#define _mp_push kv_push +#define _mp_init kvi_init +#define _mp_destroy kvi_destroy +#define _mp_push kvi_push #define _mp_pop kv_pop #define _mp_last kv_last @@ -184,10 +185,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) @@ -215,10 +218,10 @@ static inline size_t tv_strlen(const typval_T *const tv) /// tv)` which returns OK or FAIL and helper functions. /// /// @param scope Scope of the main function: either nothing or `static`. +/// @param name Name of the target converter. /// @param firstargtype Type of the first argument. It will be used to return /// the results. /// @param firstargname Name of the first argument. -/// @param name Name of the target converter. #define TYPVAL_ENCODE_DEFINE_CONV_FUNCTIONS(scope, name, firstargtype, \ firstargname) \ static int name##_convert_one_value(firstargtype firstargname, \ @@ -255,6 +258,7 @@ static int name##_convert_one_value(firstargtype firstargname, \ TYPVAL_ENCODE_CONV_LIST_START(tv->vval.v_list->lv_len); \ _mp_push(*mpstack, ((MPConvStackVal) { \ .type = kMPConvList, \ + .tv = tv, \ .data = { \ .l = { \ .list = tv->vval.v_list, \ @@ -385,6 +389,7 @@ static int name##_convert_one_value(firstargtype firstargname, \ lv_copyID, kMPConvList); \ TYPVAL_ENCODE_CONV_LIST_START(val_di->di_tv.vval.v_list->lv_len); \ _mp_push(*mpstack, ((MPConvStackVal) { \ + .tv = tv, \ .type = kMPConvList, \ .data = { \ .l = { \ @@ -415,6 +420,7 @@ static int name##_convert_one_value(firstargtype firstargname, \ kMPConvPairs); \ TYPVAL_ENCODE_CONV_DICT_START(val_list->lv_len); \ _mp_push(*mpstack, ((MPConvStackVal) { \ + .tv = tv, \ .type = kMPConvPairs, \ .data = { \ .l = { \ @@ -455,6 +461,7 @@ name##_convert_one_value_regular_dict: \ kMPConvDict); \ TYPVAL_ENCODE_CONV_DICT_START(tv->vval.v_dict->dv_hashtab.ht_used); \ _mp_push(*mpstack, ((MPConvStackVal) { \ + .tv = tv, \ .type = kMPConvDict, \ .data = { \ .d = { \ @@ -535,7 +542,7 @@ scope int encode_vim_to_##name(firstargtype firstargname, typval_T *const tv, \ } \ const list_T *const kv_pair = cur_mpsv->data.l.li->li_tv.vval.v_list; \ TYPVAL_ENCODE_CONV_SPECIAL_DICT_KEY_CHECK( \ - encode_vim_to_##name##_error_ret, kv_pair); \ + encode_vim_to_##name##_error_ret, kv_pair->lv_first->li_tv); \ if (name##_convert_one_value(firstargname, &mpstack, \ &kv_pair->lv_first->li_tv, copyID, \ objname) == FAIL) { \ diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h index 8ffc0c98ce..d5c9b2c1ec 100644 --- a/src/nvim/eval_defs.h +++ b/src/nvim/eval_defs.h @@ -155,6 +155,10 @@ typedef struct list_stack_S { /// Convert a hashitem key pointer to a dictitem pointer #define HIKEY2DI(p) ((dictitem_T *)(p - offsetof(dictitem_T, di_key))) +/// Convert a hashitem value pointer to a dictitem pointer +#define HIVAL2DI(p) \ + ((dictitem_T *)(((char *)p) - offsetof(dictitem_T, di_tv))) + /// Convert a hashitem pointer to a dictitem pointer #define HI2DI(hi) HIKEY2DI((hi)->hi_key) diff --git a/src/nvim/lib/kvec.h b/src/nvim/lib/kvec.h index 36c91c86b2..584282d773 100644 --- a/src/nvim/lib/kvec.h +++ b/src/nvim/lib/kvec.h @@ -134,6 +134,9 @@ static inline void *_memcpy_free(void *const restrict dest, /// Resize vector with preallocated array /// +/// @note May not resize to an array smaller then init_array: if requested, +/// init_array will be used. +/// /// @param[out] v Vector to resize. /// @param[in] s New size. #define kvi_resize(v, s) \ @@ -159,10 +162,10 @@ static inline void *_memcpy_free(void *const restrict dest, /* ARRAY_SIZE((v).init_array) is the minimal capacity of this vector. */ \ /* Thus when vector is full capacity may not be zero and it is safe */ \ /* not to bother with checking whether (v).capacity is 0. But now */ \ - /* capacity is not guaranteed to have size that is a power of 2. */ \ - kvi_resize(v, ((v).capacity == ARRAY_SIZE((v).init_array) \ - ? ((v).capacity++, kv_roundup32((v).capacity)) \ - : (v).capacity << 1)) + /* capacity is not guaranteed to have size that is a power of 2, it is */ \ + /* hard to fix this here and is not very necessary if users will use */ \ + /* 2^x initial array size. */ \ + kvi_resize(v, (v).capacity << 1) /// Get location where to store new element to a vector with preallocated array /// diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index 0d7d5a247e..5b249ee1c7 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -816,20 +816,55 @@ static void decref(Channel *channel) #define REQ "[request] " #define RES "[response] " #define NOT "[notification] " +#define ERR "[error] " + +// Cannot define array with negative offsets, so this one is needed to be added +// to MSGPACK_UNPACK_\* values. +#define MUR_OFF 2 + +static const char *const msgpack_error_messages[] = { + [MSGPACK_UNPACK_EXTRA_BYTES + MUR_OFF] = "extra bytes found", + [MSGPACK_UNPACK_CONTINUE + MUR_OFF] = "incomplete string", + [MSGPACK_UNPACK_PARSE_ERROR + MUR_OFF] = "parse error", + [MSGPACK_UNPACK_NOMEM_ERROR + MUR_OFF] = "not enough memory", +}; static void log_server_msg(uint64_t channel_id, msgpack_sbuffer *packed) { msgpack_unpacked unpacked; msgpack_unpacked_init(&unpacked); - msgpack_unpack_next(&unpacked, packed->data, packed->size, NULL); - uint64_t type = unpacked.data.via.array.ptr[0].via.u64; DLOGN("[msgpack-rpc] nvim -> client(%" PRIu64 ") ", channel_id); - log_lock(); - FILE *f = open_log_file(); - fprintf(f, type ? (type == 1 ? RES : NOT) : REQ); - log_msg_close(f, unpacked.data); - msgpack_unpacked_destroy(&unpacked); + const msgpack_unpack_return result = + msgpack_unpack_next(&unpacked, packed->data, packed->size, NULL); + switch (result) { + case MSGPACK_UNPACK_SUCCESS: { + uint64_t type = unpacked.data.via.array.ptr[0].via.u64; + log_lock(); + FILE *f = open_log_file(); + fprintf(f, type ? (type == 1 ? RES : NOT) : REQ); + log_msg_close(f, unpacked.data); + msgpack_unpacked_destroy(&unpacked); + break; + } + case MSGPACK_UNPACK_EXTRA_BYTES: + case MSGPACK_UNPACK_CONTINUE: + case MSGPACK_UNPACK_PARSE_ERROR: + case MSGPACK_UNPACK_NOMEM_ERROR: { + log_lock(); + FILE *f = open_log_file(); + fprintf(f, ERR); + log_msg_close(f, (msgpack_object) { + .type = MSGPACK_OBJECT_STR, + .via.str = { + .ptr = (char *)msgpack_error_messages[result + MUR_OFF], + .size = (uint32_t)strlen( + msgpack_error_messages[result + MUR_OFF]), + }, + }); + break; + } + } } static void log_client_msg(uint64_t channel_id, diff --git a/src/nvim/msgpack_rpc/helpers.c b/src/nvim/msgpack_rpc/helpers.c index c643bae0e8..9195b10614 100644 --- a/src/nvim/msgpack_rpc/helpers.c +++ b/src/nvim/msgpack_rpc/helpers.c @@ -7,9 +7,11 @@ #include "nvim/api/private/helpers.h" #include "nvim/msgpack_rpc/helpers.h" #include "nvim/msgpack_rpc/defs.h" +#include "nvim/lib/kvec.h" #include "nvim/vim.h" #include "nvim/log.h" #include "nvim/memory.h" +#include "nvim/assert.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "msgpack_rpc/helpers.c.generated.h" @@ -19,7 +21,7 @@ static msgpack_zone zone; static msgpack_sbuffer sbuffer; #define HANDLE_TYPE_CONVERSION_IMPL(t, lt) \ - bool msgpack_rpc_to_##lt(msgpack_object *obj, t *arg) \ + bool msgpack_rpc_to_##lt(const msgpack_object *const obj, t *const arg) \ FUNC_ATTR_NONNULL_ALL \ { \ if (obj->type != MSGPACK_OBJECT_EXT \ @@ -63,34 +65,182 @@ HANDLE_TYPE_CONVERSION_IMPL(Buffer, buffer) HANDLE_TYPE_CONVERSION_IMPL(Window, window) HANDLE_TYPE_CONVERSION_IMPL(Tabpage, tabpage) -bool msgpack_rpc_to_boolean(msgpack_object *obj, Boolean *arg) +typedef struct { + const msgpack_object *mobj; + Object *aobj; + bool container; + size_t idx; +} MPToAPIObjectStackItem; + +/// Convert type used by msgpack parser to Neovim own API type +/// +/// @param[in] obj Msgpack value to convert. +/// @param[out] arg Location where result of conversion will be saved. +/// +/// @return true in case of success, false otherwise. +bool msgpack_rpc_to_object(const msgpack_object *const obj, Object *const arg) FUNC_ATTR_NONNULL_ALL { - *arg = obj->via.boolean; - return obj->type == MSGPACK_OBJECT_BOOLEAN; -} - -bool msgpack_rpc_to_integer(msgpack_object *obj, Integer *arg) - FUNC_ATTR_NONNULL_ALL -{ - if (obj->type == MSGPACK_OBJECT_POSITIVE_INTEGER - && obj->via.u64 <= INT64_MAX) { - *arg = (int64_t)obj->via.u64; - return true; + bool ret = true; + kvec_t(MPToAPIObjectStackItem) stack = KV_INITIAL_VALUE; + kv_push(stack, ((MPToAPIObjectStackItem) { obj, arg, false, 0 })); + while (ret && kv_size(stack)) { + MPToAPIObjectStackItem cur = kv_last(stack); + if (!cur.container) { + *cur.aobj = NIL; + } + switch (cur.mobj->type) { + case MSGPACK_OBJECT_NIL: { + break; + } + case MSGPACK_OBJECT_BOOLEAN: { + *cur.aobj = BOOLEAN_OBJ(cur.mobj->via.boolean); + break; + } + case MSGPACK_OBJECT_NEGATIVE_INTEGER: { + STATIC_ASSERT(sizeof(Integer) == sizeof(cur.mobj->via.i64), + "Msgpack integer size does not match API integer"); + *cur.aobj = INTEGER_OBJ(cur.mobj->via.i64); + break; + } + case MSGPACK_OBJECT_POSITIVE_INTEGER: { + STATIC_ASSERT(sizeof(Integer) == sizeof(cur.mobj->via.u64), + "Msgpack integer size does not match API integer"); + if (cur.mobj->via.u64 > API_INTEGER_MAX) { + ret = false; + } else { + *cur.aobj = INTEGER_OBJ((Integer)cur.mobj->via.u64); + } + break; + } + case MSGPACK_OBJECT_FLOAT: { + STATIC_ASSERT(sizeof(Float) == sizeof(cur.mobj->via.f64), + "Msgpack floating-point size does not match API integer"); + *cur.aobj = FLOATING_OBJ(cur.mobj->via.f64); + break; + } +#define STR_CASE(type, attr, obj, dest, conv) \ + case type: { \ + dest = conv(((String) { \ + .size = obj->via.attr.size, \ + .data = (obj->via.attr.ptr == NULL || obj->via.attr.size == 0 \ + ? NULL \ + : xmemdupz(obj->via.attr.ptr, obj->via.attr.size)), \ + })); \ + break; \ + } + STR_CASE(MSGPACK_OBJECT_STR, str, cur.mobj, *cur.aobj, STRING_OBJ) + STR_CASE(MSGPACK_OBJECT_BIN, bin, cur.mobj, *cur.aobj, STRING_OBJ) + case MSGPACK_OBJECT_ARRAY: { + const size_t size = cur.mobj->via.array.size; + if (cur.container) { + if (cur.idx >= size) { + (void)kv_pop(stack); + } else { + const size_t idx = cur.idx; + cur.idx++; + kv_last(stack) = cur; + kv_push(stack, ((MPToAPIObjectStackItem) { + .mobj = &cur.mobj->via.array.ptr[idx], + .aobj = &cur.aobj->data.array.items[idx], + .container = false, + })); + } + } else { + *cur.aobj = ARRAY_OBJ(((Array) { + .size = size, + .capacity = size, + .items = (size > 0 + ? xcalloc(size, sizeof(*cur.aobj->data.array.items)) + : NULL), + })); + cur.container = true; + kv_last(stack) = cur; + } + break; + } + case MSGPACK_OBJECT_MAP: { + const size_t size = cur.mobj->via.map.size; + if (cur.container) { + if (cur.idx >= size) { + (void)kv_pop(stack); + } else { + const size_t idx = cur.idx; + cur.idx++; + kv_last(stack) = cur; + const msgpack_object *const key = &cur.mobj->via.map.ptr[idx].key; + switch (key->type) { +#define ID(x) x + STR_CASE(MSGPACK_OBJECT_STR, str, key, + cur.aobj->data.dictionary.items[idx].key, ID) + STR_CASE(MSGPACK_OBJECT_BIN, bin, key, + cur.aobj->data.dictionary.items[idx].key, ID) +#undef ID + case MSGPACK_OBJECT_NIL: + case MSGPACK_OBJECT_BOOLEAN: + case MSGPACK_OBJECT_POSITIVE_INTEGER: + case MSGPACK_OBJECT_NEGATIVE_INTEGER: + case MSGPACK_OBJECT_FLOAT: + case MSGPACK_OBJECT_EXT: + case MSGPACK_OBJECT_MAP: + case MSGPACK_OBJECT_ARRAY: { + ret = false; + break; + } + } + if (ret) { + kv_push(stack, ((MPToAPIObjectStackItem) { + .mobj = &cur.mobj->via.map.ptr[idx].val, + .aobj = &cur.aobj->data.dictionary.items[idx].value, + .container = false, + })); + } + } + } else { + *cur.aobj = DICTIONARY_OBJ(((Dictionary) { + .size = size, + .capacity = size, + .items = (size > 0 + ? xcalloc(size, sizeof(*cur.aobj->data.dictionary.items)) + : NULL), + })); + cur.container = true; + kv_last(stack) = cur; + } + break; + } + case MSGPACK_OBJECT_EXT: { + switch (cur.mobj->via.ext.type) { + case kObjectTypeBuffer: { + cur.aobj->type = kObjectTypeBuffer; + ret = msgpack_rpc_to_buffer(cur.mobj, &cur.aobj->data.buffer); + break; + } + case kObjectTypeWindow: { + cur.aobj->type = kObjectTypeWindow; + ret = msgpack_rpc_to_window(cur.mobj, &cur.aobj->data.window); + break; + } + case kObjectTypeTabpage: { + cur.aobj->type = kObjectTypeTabpage; + ret = msgpack_rpc_to_tabpage(cur.mobj, &cur.aobj->data.tabpage); + break; + } + } + break; + } +#undef STR_CASE + } + if (!cur.container) { + (void)kv_pop(stack); + } } - - *arg = obj->via.i64; - return obj->type == MSGPACK_OBJECT_NEGATIVE_INTEGER; + kv_destroy(stack); + return ret; } -bool msgpack_rpc_to_float(msgpack_object *obj, Float *arg) - FUNC_ATTR_NONNULL_ALL -{ - *arg = obj->via.f64; - return obj->type == MSGPACK_OBJECT_FLOAT; -} - -bool msgpack_rpc_to_string(msgpack_object *obj, String *arg) +static bool msgpack_rpc_to_string(const msgpack_object *const obj, + String *const arg) FUNC_ATTR_NONNULL_ALL { if (obj->type == MSGPACK_OBJECT_BIN || obj->type == MSGPACK_OBJECT_STR) { @@ -103,58 +253,7 @@ bool msgpack_rpc_to_string(msgpack_object *obj, String *arg) return false; } -bool msgpack_rpc_to_object(msgpack_object *obj, Object *arg) - FUNC_ATTR_NONNULL_ALL -{ - switch (obj->type) { - case MSGPACK_OBJECT_NIL: - arg->type = kObjectTypeNil; - return true; - - case MSGPACK_OBJECT_BOOLEAN: - arg->type = kObjectTypeBoolean; - return msgpack_rpc_to_boolean(obj, &arg->data.boolean); - - case MSGPACK_OBJECT_POSITIVE_INTEGER: - case MSGPACK_OBJECT_NEGATIVE_INTEGER: - arg->type = kObjectTypeInteger; - return msgpack_rpc_to_integer(obj, &arg->data.integer); - - case MSGPACK_OBJECT_FLOAT: - arg->type = kObjectTypeFloat; - return msgpack_rpc_to_float(obj, &arg->data.floating); - - case MSGPACK_OBJECT_BIN: - case MSGPACK_OBJECT_STR: - arg->type = kObjectTypeString; - return msgpack_rpc_to_string(obj, &arg->data.string); - - case MSGPACK_OBJECT_ARRAY: - arg->type = kObjectTypeArray; - return msgpack_rpc_to_array(obj, &arg->data.array); - - case MSGPACK_OBJECT_MAP: - arg->type = kObjectTypeDictionary; - return msgpack_rpc_to_dictionary(obj, &arg->data.dictionary); - - case MSGPACK_OBJECT_EXT: - switch (obj->via.ext.type) { - case kObjectTypeBuffer: - arg->type = kObjectTypeBuffer; - return msgpack_rpc_to_buffer(obj, &arg->data.buffer); - case kObjectTypeWindow: - arg->type = kObjectTypeWindow; - return msgpack_rpc_to_window(obj, &arg->data.window); - case kObjectTypeTabpage: - arg->type = kObjectTypeTabpage; - return msgpack_rpc_to_tabpage(obj, &arg->data.tabpage); - } - default: - return false; - } -} - -bool msgpack_rpc_to_array(msgpack_object *obj, Array *arg) +bool msgpack_rpc_to_array(const msgpack_object *const obj, Array *const arg) FUNC_ATTR_NONNULL_ALL { if (obj->type != MSGPACK_OBJECT_ARRAY) { @@ -173,7 +272,8 @@ bool msgpack_rpc_to_array(msgpack_object *obj, Array *arg) return true; } -bool msgpack_rpc_to_dictionary(msgpack_object *obj, Dictionary *arg) +bool msgpack_rpc_to_dictionary(const msgpack_object *const obj, + Dictionary *const arg) FUNC_ATTR_NONNULL_ALL { if (obj->type != MSGPACK_OBJECT_MAP) { @@ -228,50 +328,108 @@ void msgpack_rpc_from_string(String result, msgpack_packer *res) msgpack_pack_str_body(res, result.data, result.size); } -void msgpack_rpc_from_object(Object result, msgpack_packer *res) +typedef struct { + const Object *aobj; + bool container; + size_t idx; +} APIToMPObjectStackItem; + +/// Convert type used by Neovim API to msgpack +/// +/// @param[in] result Object to convert. +/// @param[out] res Structure that defines where conversion results are saved. +/// +/// @return true in case of success, false otherwise. +void msgpack_rpc_from_object(const Object result, msgpack_packer *const res) FUNC_ATTR_NONNULL_ARG(2) { - switch (result.type) { - case kObjectTypeNil: - msgpack_pack_nil(res); - break; - - case kObjectTypeBoolean: - msgpack_rpc_from_boolean(result.data.boolean, res); - break; - - case kObjectTypeInteger: - msgpack_rpc_from_integer(result.data.integer, res); - break; - - case kObjectTypeFloat: - msgpack_rpc_from_float(result.data.floating, res); - break; - - case kObjectTypeString: - msgpack_rpc_from_string(result.data.string, res); - break; - - case kObjectTypeArray: - msgpack_rpc_from_array(result.data.array, res); - break; - - case kObjectTypeBuffer: - msgpack_rpc_from_buffer(result.data.buffer, res); - break; - - case kObjectTypeWindow: - msgpack_rpc_from_window(result.data.window, res); - break; - - case kObjectTypeTabpage: - msgpack_rpc_from_tabpage(result.data.tabpage, res); - break; - - case kObjectTypeDictionary: - msgpack_rpc_from_dictionary(result.data.dictionary, res); - break; + kvec_t(APIToMPObjectStackItem) stack = KV_INITIAL_VALUE; + kv_push(stack, ((APIToMPObjectStackItem) { &result, false, 0 })); + while (kv_size(stack)) { + APIToMPObjectStackItem cur = kv_last(stack); + switch (cur.aobj->type) { + case kObjectTypeNil: { + msgpack_pack_nil(res); + break; + } + case kObjectTypeBoolean: { + msgpack_rpc_from_boolean(cur.aobj->data.boolean, res); + break; + } + case kObjectTypeInteger: { + msgpack_rpc_from_integer(cur.aobj->data.integer, res); + break; + } + case kObjectTypeFloat: { + msgpack_rpc_from_float(cur.aobj->data.floating, res); + break; + } + case kObjectTypeString: { + msgpack_rpc_from_string(cur.aobj->data.string, res); + break; + } + case kObjectTypeBuffer: { + msgpack_rpc_from_buffer(cur.aobj->data.buffer, res); + break; + } + case kObjectTypeWindow: { + msgpack_rpc_from_window(cur.aobj->data.window, res); + break; + } + case kObjectTypeTabpage: { + msgpack_rpc_from_tabpage(cur.aobj->data.tabpage, res); + break; + } + case kObjectTypeArray: { + const size_t size = cur.aobj->data.array.size; + if (cur.container) { + if (cur.idx >= size) { + (void)kv_pop(stack); + } else { + const size_t idx = cur.idx; + cur.idx++; + kv_last(stack) = cur; + kv_push(stack, ((APIToMPObjectStackItem) { + .aobj = &cur.aobj->data.array.items[idx], + .container = false, + })); + } + } else { + msgpack_pack_array(res, size); + cur.container = true; + kv_last(stack) = cur; + } + break; + } + case kObjectTypeDictionary: { + const size_t size = cur.aobj->data.dictionary.size; + if (cur.container) { + if (cur.idx >= size) { + (void)kv_pop(stack); + } else { + const size_t idx = cur.idx; + cur.idx++; + kv_last(stack) = cur; + msgpack_rpc_from_string(cur.aobj->data.dictionary.items[idx].key, + res); + kv_push(stack, ((APIToMPObjectStackItem) { + .aobj = &cur.aobj->data.dictionary.items[idx].value, + .container = false, + })); + } + } else { + msgpack_pack_map(res, size); + cur.container = true; + kv_last(stack) = cur; + } + break; + } + } + if (!cur.container) { + (void)kv_pop(stack); + } } + kv_destroy(stack); } void msgpack_rpc_from_array(Array result, msgpack_packer *res) diff --git a/src/nvim/option.c b/src/nvim/option.c index 52a1fd8558..020a119fd3 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -2314,50 +2314,46 @@ set_string_option_global ( } } -/* - * Set a string option to a new value, and handle the effects. - * - * Returns NULL on success or error message on error. - */ -static char_u * -set_string_option ( - int opt_idx, - char_u *value, - int opt_flags /* OPT_LOCAL and/or OPT_GLOBAL */ -) +/// Set a string option to a new value, handling the effects +/// +/// @param[in] opt_idx Option to set. +/// @param[in] value New value. +/// @param[in] opt_flags Option flags: expected to contain #OPT_LOCAL and/or +/// #OPT_GLOBAL. +/// +/// @return NULL on success, error message on error. +static char *set_string_option(const int opt_idx, const char *const value, + const int opt_flags) + FUNC_ATTR_NONNULL_ARG(2) FUNC_ATTR_WARN_UNUSED_RESULT { - char_u *s; - char_u **varp; - char_u *oldval; - char *saved_oldval = NULL; - char_u *r = NULL; - - if (options[opt_idx].var == NULL) /* don't set hidden option */ + if (options[opt_idx].var == NULL) { // don't set hidden option return NULL; + } - s = vim_strsave(value); - varp = (char_u **)get_varp_scope(&(options[opt_idx]), - (opt_flags & (OPT_LOCAL | OPT_GLOBAL)) == 0 - ? (((int)options[opt_idx].indir & PV_BOTH) - ? OPT_GLOBAL : OPT_LOCAL) - : opt_flags); - oldval = *varp; + char *const s = xstrdup(value); + char **const varp = (char **)get_varp_scope( + &(options[opt_idx]), + ((opt_flags & (OPT_LOCAL | OPT_GLOBAL)) == 0 + ? (((int)options[opt_idx].indir & PV_BOTH) + ? OPT_GLOBAL : OPT_LOCAL) + : opt_flags)); + char *const oldval = *varp; *varp = s; - if (!starting) { - saved_oldval = xstrdup((char *) oldval); - } + char *const saved_oldval = (starting ? NULL : xstrdup(oldval)); - if ((r = did_set_string_option(opt_idx, varp, (int)true, oldval, NULL, - opt_flags)) == NULL) - did_set_option(opt_idx, opt_flags, TRUE); + char *const r = (char *)did_set_string_option( + opt_idx, (char_u **)varp, (int)true, (char_u *)oldval, NULL, opt_flags); + if (r == NULL) { + did_set_option(opt_idx, opt_flags, true); + } // call autocommand after handling side effects if (saved_oldval != NULL) { char buf_type[7]; vim_snprintf(buf_type, ARRAY_SIZE(buf_type), "%s", (opt_flags & OPT_LOCAL) ? "local" : "global"); - set_vim_var_string(VV_OPTION_NEW, (char *) (*varp), -1); + set_vim_var_string(VV_OPTION_NEW, (char *)(*varp), -1); set_vim_var_string(VV_OPTION_OLD, saved_oldval, -1); set_vim_var_string(VV_OPTION_TYPE, buf_type, -1); apply_autocmds(EVENT_OPTIONSET, @@ -4655,26 +4651,28 @@ set_option_value ( EMSG(_(e_sandbox)); return NULL; } - if (flags & P_STRING) - return set_string_option(opt_idx, string, opt_flags); - else { + if (flags & P_STRING) { + const char *s = (const char *)string; + if (s == NULL) { + s = ""; + } + return (char_u *)set_string_option(opt_idx, s, opt_flags); + } else { varp = get_varp_scope(&(options[opt_idx]), opt_flags); if (varp != NULL) { /* hidden option is not changed */ if (number == 0 && string != NULL) { int idx; - /* Either we are given a string or we are setting option - * to zero. */ - for (idx = 0; string[idx] == '0'; ++idx) - ; + // Either we are given a string or we are setting option + // to zero. + for (idx = 0; string[idx] == '0'; idx++) {} if (string[idx] != NUL || idx == 0) { - /* There's another character after zeros or the string - * is empty. In both cases, we are trying to set a - * num option using a string. */ + // There's another character after zeros or the string + // is empty. In both cases, we are trying to set a + // num option using a string. EMSG3(_("E521: Number required: &%s = '%s'"), - name, string); - return NULL; /* do nothing as we hit an error */ - + name, string); + return NULL; // do nothing as we hit an error } } if (flags & P_NUM) diff --git a/src/nvim/os/fileio.c b/src/nvim/os/fileio.c new file mode 100644 index 0000000000..6cee102305 --- /dev/null +++ b/src/nvim/os/fileio.c @@ -0,0 +1,319 @@ +/// @file fileio.c +/// +/// Buffered reading/writing to a file. Unlike fileio.c this is not dealing with +/// Neovim stuctures for buffer, with autocommands, etc: just fopen/fread/fwrite +/// replacement. + +#include <unistd.h> +#include <assert.h> +#include <stddef.h> +#include <stdbool.h> +#include <fcntl.h> + +#include "auto/config.h" + +#ifdef HAVE_SYS_UIO_H +# include <sys/uio.h> +#endif + +#include <uv.h> + +#include "nvim/os/fileio.h" +#include "nvim/memory.h" +#include "nvim/os/os.h" +#include "nvim/globals.h" +#include "nvim/rbuffer.h" +#include "nvim/macros.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/fileio.c.generated.h" +#endif + +/// Open file +/// +/// @param[out] ret_fp Address where information needed for reading from or +/// writing to a file is saved +/// @param[in] fname File name to open. +/// @param[in] flags Flags, @see FileOpenFlags. Currently reading from and +/// writing to the file at once is not supported, so either +/// FILE_WRITE_ONLY or FILE_READ_ONLY is required. +/// @param[in] mode Permissions for the newly created file (ignored if flags +/// does not have FILE_CREATE\*). +/// +/// @return Error code (@see os_strerror()) or 0. +int file_open(FileDescriptor *const ret_fp, const char *const fname, + const int flags, const int mode) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + int os_open_flags = 0; + int fd; + TriState wr = kNone; +#define FLAG(flags, flag, fcntl_flags, wrval, cond) \ + do { \ + if (flags & flag) { \ + os_open_flags |= fcntl_flags; \ + assert(cond); \ + if (wrval != kNone) { \ + wr = wrval; \ + } \ + } \ + } while (0) + FLAG(flags, kFileWriteOnly, O_WRONLY, kTrue, true); + FLAG(flags, kFileCreateOnly, O_CREAT|O_EXCL|O_WRONLY, kTrue, true); + FLAG(flags, kFileCreate, O_CREAT|O_WRONLY, kTrue, !(flags & kFileCreateOnly)); + FLAG(flags, kFileTruncate, O_TRUNC|O_WRONLY, kTrue, + !(flags & kFileCreateOnly)); + FLAG(flags, kFileReadOnly, O_RDONLY, kFalse, wr != kTrue); +#ifdef O_NOFOLLOW + FLAG(flags, kFileNoSymlink, O_NOFOLLOW, kNone, true); +#endif +#undef FLAG + + fd = os_open(fname, os_open_flags, mode); + + if (fd < 0) { + return fd; + } + + ret_fp->wr = (wr == kTrue); + ret_fp->fd = fd; + ret_fp->eof = false; + ret_fp->rv = rbuffer_new(kRWBufferSize); + ret_fp->_error = 0; + if (ret_fp->wr) { + ret_fp->rv->data = ret_fp; + ret_fp->rv->full_cb = (rbuffer_callback)&file_rb_write_full_cb; + } + return 0; +} + +/// Like file_open(), but allocate and return ret_fp +/// +/// @param[out] error Error code, @see os_strerror(). Is set to zero on +/// success. +/// @param[in] fname File name to open. +/// @param[in] flags Flags, @see FileOpenFlags. +/// @param[in] mode Permissions for the newly created file (ignored if flags +/// does not have FILE_CREATE\*). +/// +/// @return [allocated] Opened file or NULL in case of error. +FileDescriptor *file_open_new(int *const error, const char *const fname, + const int flags, const int mode) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + FileDescriptor *const fp = xmalloc(sizeof(*fp)); + if ((*error = file_open(fp, fname, flags, mode)) != 0) { + xfree(fp); + return NULL; + } + return fp; +} + +/// Close file and free its buffer +/// +/// @param[in,out] fp File to close. +/// +/// @return 0 or error code. +int file_close(FileDescriptor *const fp) FUNC_ATTR_NONNULL_ALL +{ + const int error = file_fsync(fp); + const int error2 = os_close(fp->fd); + rbuffer_free(fp->rv); + if (error2 != 0) { + return error2; + } + return error; +} + +/// Close and free file obtained using file_open_new() +/// +/// @param[in,out] fp File to close. +/// +/// @return 0 or error code. +int file_free(FileDescriptor *const fp) FUNC_ATTR_NONNULL_ALL +{ + const int ret = file_close(fp); + xfree(fp); + return ret; +} + +/// Flush file modifications to disk +/// +/// @param[in,out] fp File to work with. +/// +/// @return 0 or error code. +int file_fsync(FileDescriptor *const fp) + FUNC_ATTR_NONNULL_ALL +{ + if (!fp->wr) { + return 0; + } + file_rb_write_full_cb(fp->rv, fp); + if (fp->_error != 0) { + const int error = fp->_error; + fp->_error = 0; + return error; + } + return os_fsync(fp->fd); +} + +/// Buffer used for writing +/// +/// Like IObuff, but allows file_\* callers not to care about spoiling it. +static char writebuf[kRWBufferSize]; + +/// Function run when RBuffer is full when writing to a file +/// +/// Actually does writing to the file, may also be invoked directly. +/// +/// @param[in,out] rv RBuffer instance used. +/// @param[in,out] fp File to work with. +static void file_rb_write_full_cb(RBuffer *const rv, FileDescriptor *const fp) + FUNC_ATTR_NONNULL_ALL +{ + assert(fp->wr); + assert(rv->data == (void *)fp); + if (rbuffer_size(rv) == 0) { + return; + } + const size_t read_bytes = rbuffer_read(rv, writebuf, kRWBufferSize); + const ptrdiff_t wres = os_write(fp->fd, writebuf, read_bytes); + if (wres != (ptrdiff_t)read_bytes) { + if (wres >= 0) { + fp->_error = UV_EIO; + } else { + fp->_error = (int)wres; + } + } +} + +/// Read from file +/// +/// @param[in,out] fp File to work with. +/// @param[out] ret_buf Buffer to read to. Must not be NULL. +/// @param[in] size Number of bytes to read. Buffer must have at least ret_buf +/// bytes. +/// +/// @return error_code (< 0) or number of bytes read. +ptrdiff_t file_read(FileDescriptor *const fp, char *const ret_buf, + const size_t size) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + assert(!fp->wr); + char *buf = ret_buf; + size_t read_remaining = size; + RBuffer *const rv = fp->rv; + while (read_remaining) { + const size_t rv_size = rbuffer_size(rv); + if (rv_size > 0) { + const size_t rsize = rbuffer_read(rv, buf, MIN(rv_size, read_remaining)); + buf += rsize; + read_remaining -= rsize; + } + if (fp->eof) { + break; + } + if (read_remaining) { + assert(rbuffer_size(rv) == 0); + rbuffer_reset(rv); +#ifdef HAVE_READV + // If there is readv() syscall, then take an opportunity to populate + // both target buffer and RBuffer at once, … + size_t write_count; + struct iovec iov[] = { + { .iov_base = buf, .iov_len = read_remaining }, + { .iov_base = rbuffer_write_ptr(rv, &write_count), + .iov_len = kRWBufferSize }, + }; + assert(write_count == kRWBufferSize); + const ptrdiff_t r_ret = os_readv(fp->fd, &fp->eof, iov, + ARRAY_SIZE(iov)); + if (r_ret > 0) { + if (r_ret > (ptrdiff_t)read_remaining) { + rbuffer_produced(rv, (size_t)(r_ret - (ptrdiff_t)read_remaining)); + read_remaining = 0; + } else { + buf += (size_t)r_ret; + read_remaining -= (size_t)r_ret; + } + } else if (r_ret < 0) { + return r_ret; + } +#else + if (read_remaining >= kRWBufferSize) { + // …otherwise leave RBuffer empty and populate only target buffer, + // because filtering information through rbuffer will be more syscalls. + const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof, buf, read_remaining); + if (r_ret >= 0) { + read_remaining -= (size_t)r_ret; + return (ptrdiff_t)(size - read_remaining); + } else if (r_ret < 0) { + return r_ret; + } + } else { + size_t write_count; + const ptrdiff_t r_ret = os_read(fp->fd, &fp->eof, + rbuffer_write_ptr(rv, &write_count), + kRWBufferSize); + assert(write_count == kRWBufferSize); + if (r_ret > 0) { + rbuffer_produced(rv, (size_t)r_ret); + } else if (r_ret < 0) { + return r_ret; + } + } +#endif + } + } + return (ptrdiff_t)(size - read_remaining); +} + +/// Write to a file +/// +/// @param[in] fd File descriptor to write to. +/// @param[in] buf Data to write. May be NULL if size is zero. +/// @param[in] size Amount of bytes to write. +/// +/// @return Number of bytes written or libuv error code (< 0). +ptrdiff_t file_write(FileDescriptor *const fp, const char *const buf, + const size_t size) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(1) +{ + assert(fp->wr); + const size_t written = rbuffer_write(fp->rv, buf, size); + if (fp->_error != 0) { + const int error = fp->_error; + fp->_error = 0; + return error; + } else if (written != size) { + return UV_EIO; + } + return (ptrdiff_t)written; +} + +/// Buffer used for skipping. Its contents is undefined and should never be +/// used. +static char skipbuf[kRWBufferSize]; + +/// Skip some bytes +/// +/// This is like `fseek(fp, size, SEEK_CUR)`, but actual implementation simply +/// reads to a buffer and discards the result. +ptrdiff_t file_skip(FileDescriptor *const fp, const size_t size) + FUNC_ATTR_NONNULL_ALL +{ + assert(!fp->wr); + size_t read_bytes = 0; + do { + const ptrdiff_t new_read_bytes = file_read( + fp, skipbuf, MIN(size - read_bytes, sizeof(skipbuf))); + if (new_read_bytes < 0) { + return new_read_bytes; + } else if (new_read_bytes == 0) { + break; + } + read_bytes += (size_t)new_read_bytes; + } while (read_bytes < size && !file_eof(fp)); + + return (ptrdiff_t)read_bytes; +} diff --git a/src/nvim/os/fileio.h b/src/nvim/os/fileio.h new file mode 100644 index 0000000000..2cffd5c851 --- /dev/null +++ b/src/nvim/os/fileio.h @@ -0,0 +1,72 @@ +#ifndef NVIM_OS_FILEIO_H +#define NVIM_OS_FILEIO_H + +#include <stdbool.h> +#include <stddef.h> + +#include "nvim/func_attr.h" +#include "nvim/rbuffer.h" + +/// Structure used to read from/write to file +typedef struct { + int fd; ///< File descriptor. + int _error; ///< Error code for use with RBuffer callbacks or zero. + RBuffer *rv; ///< Read or write buffer. + bool wr; ///< True if file is in write mode. + bool eof; ///< True if end of file was encountered. +} FileDescriptor; + +/// file_open() flags +typedef enum { + kFileReadOnly = 1, ///< Open file read-only. Default. + kFileCreate = 2, ///< Create file if it does not exist yet. + ///< Implies kFileWriteOnly. + kFileWriteOnly = 4, ///< Open file for writing only. + ///< Cannot be used with kFileReadOnly. + kFileNoSymlink = 8, ///< Do not allow symbolic links. + kFileCreateOnly = 16, ///< Only create the file, failing if it already + ///< exists. Implies kFileWriteOnly. Cannot be used + ///< with kFileCreate. + kFileTruncate = 32, ///< Truncate the file if it exists. + ///< Implies kFileWriteOnly. Cannot be used with + ///< kFileCreateOnly. +} FileOpenFlags; + +static inline bool file_eof(const FileDescriptor *const fp) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL; + +/// Check whether end of file was encountered +/// +/// @param[in] fp File to check. +/// +/// @return true if it was, false if it was not or read operation was never +/// performed. +static inline bool file_eof(const FileDescriptor *const fp) +{ + return fp->eof && rbuffer_size(fp->rv) == 0; +} + +static inline int file_fd(const FileDescriptor *const fp) + REAL_FATTR_PURE REAL_FATTR_WARN_UNUSED_RESULT REAL_FATTR_NONNULL_ALL; + +/// Return the file descriptor associated with the FileDescriptor structure +/// +/// @param[in] fp File to check. +/// +/// @return File descriptor. +static inline int file_fd(const FileDescriptor *const fp) +{ + return fp->fd; +} + +enum { + /// Read or write buffer size + /// + /// Currently equal to (IOSIZE - 1), but they do not need to be connected. + kRWBufferSize = 1024 +}; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/fileio.h.generated.h" +#endif +#endif // NVIM_OS_FILEIO_H diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 08122828bd..49c42cb63d 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -1,14 +1,26 @@ // fs.c -- filesystem access #include <stdbool.h> - +#include <stddef.h> #include <assert.h> +#include <limits.h> +#include <unistd.h> #include <fcntl.h> +#include <errno.h> + +#include "auto/config.h" + +#ifdef HAVE_SYS_UIO_H +# include <sys/uio.h> +#endif + +#include <uv.h> #include "nvim/os/os.h" #include "nvim/os/os_defs.h" #include "nvim/ascii.h" #include "nvim/memory.h" #include "nvim/message.h" +#include "nvim/assert.h" #include "nvim/misc1.h" #include "nvim/misc2.h" #include "nvim/path.h" @@ -18,6 +30,20 @@ # include "os/fs.c.generated.h" #endif +#define RUN_UV_FS_FUNC(ret, func, ...) \ + do { \ + bool did_try_to_free = false; \ +uv_call_start: {} \ + uv_fs_t req; \ + ret = func(&fs_loop, &req, __VA_ARGS__); \ + uv_fs_req_cleanup(&req); \ + if (ret == UV_ENOMEM && !did_try_to_free) { \ + try_to_free_memory(); \ + did_try_to_free = true; \ + goto uv_call_start; \ + } \ + } while (0) + // Many fs functions from libuv return that value on success. static const int kLibuvSuccess = 0; static uv_loop_t fs_loop; @@ -325,13 +351,190 @@ static bool is_executable_in_path(const char_u *name, char_u **abspath) int os_open(const char* path, int flags, int mode) FUNC_ATTR_NONNULL_ALL { - uv_fs_t open_req; - int r = uv_fs_open(&fs_loop, &open_req, path, flags, mode, NULL); - uv_fs_req_cleanup(&open_req); - // r is the same as open_req.result (except for OOM: then only r is set). + int r; + RUN_UV_FS_FUNC(r, uv_fs_open, path, flags, mode, NULL); + return r; +} + +/// Close a file +/// +/// @return 0 or libuv error code on failure. +int os_close(const int fd) +{ + int r; + RUN_UV_FS_FUNC(r, uv_fs_close, fd, NULL); return r; } +/// Read from a file +/// +/// Handles EINTR and ENOMEM, but not other errors. +/// +/// @param[in] fd File descriptor to read from. +/// @param[out] ret_eof Is set to true if EOF was encountered, otherwise set +/// to false. Initial value is ignored. +/// @param[out] ret_buf Buffer to write to. May be NULL if size is zero. +/// @param[in] size Amount of bytes to read. +/// +/// @return Number of bytes read or libuv error code (< 0). +ptrdiff_t os_read(const int fd, bool *ret_eof, char *const ret_buf, + const size_t size) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + *ret_eof = false; + if (ret_buf == NULL) { + assert(size == 0); + return 0; + } + size_t read_bytes = 0; + bool did_try_to_free = false; + while (read_bytes != size) { + const ptrdiff_t cur_read_bytes = read(fd, ret_buf + read_bytes, + size - read_bytes); + if (cur_read_bytes > 0) { + read_bytes += (size_t)cur_read_bytes; + assert(read_bytes <= size); + } + if (cur_read_bytes < 0) { +#ifdef HAVE_UV_TRANSLATE_SYS_ERROR + const int error = uv_translate_sys_error(errno); +#else + const int error = -errno; + STATIC_ASSERT(-EINTR == UV_EINTR, "Need to translate error codes"); + STATIC_ASSERT(-EAGAIN == UV_EAGAIN, "Need to translate error codes"); + STATIC_ASSERT(-ENOMEM == UV_ENOMEM, "Need to translate error codes"); +#endif + errno = 0; + if (error == UV_EINTR || error == UV_EAGAIN) { + continue; + } else if (error == UV_ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + continue; + } else { + return (ptrdiff_t)error; + } + } + if (cur_read_bytes == 0) { + *ret_eof = true; + break; + } + } + return (ptrdiff_t)read_bytes; +} + +#ifdef HAVE_READV +/// Read from a file to multiple buffers at once +/// +/// Wrapper for readv(). +/// +/// @param[in] fd File descriptor to read from. +/// @param[out] ret_eof Is set to true if EOF was encountered, otherwise set +/// to false. Initial value is ignored. +/// @param[out] iov Description of buffers to write to. Note: this description +/// may change, it is incorrect to use data it points to after +/// os_readv(). +/// @param[in] iov_size Number of buffers in iov. +ptrdiff_t os_readv(int fd, bool *ret_eof, struct iovec *iov, size_t iov_size) + FUNC_ATTR_NONNULL_ALL +{ + *ret_eof = false; + size_t read_bytes = 0; + bool did_try_to_free = false; + size_t toread = 0; + for (size_t i = 0; i < iov_size; i++) { + // Overflow, trying to read too much data + assert(toread <= SIZE_MAX - iov[i].iov_len); + toread += iov[i].iov_len; + } + while (read_bytes < toread && iov_size && !*ret_eof) { + ptrdiff_t cur_read_bytes = readv(fd, iov, (int)iov_size); + if (toread && cur_read_bytes == 0) { + *ret_eof = true; + } + if (cur_read_bytes > 0) { + read_bytes += (size_t)cur_read_bytes; + while (iov_size && cur_read_bytes) { + if (cur_read_bytes < (ptrdiff_t)iov->iov_len) { + iov->iov_len -= (size_t)cur_read_bytes; + iov->iov_base = (char *)iov->iov_base + cur_read_bytes; + cur_read_bytes = 0; + } else { + cur_read_bytes -= (ptrdiff_t)iov->iov_len; + iov_size--; + iov++; + } + } + } else if (cur_read_bytes < 0) { +#ifdef HAVE_UV_TRANSLATE_SYS_ERROR + const int error = uv_translate_sys_error(errno); +#else + const int error = -errno; + STATIC_ASSERT(-EINTR == UV_EINTR, "Need to translate error codes"); + STATIC_ASSERT(-EAGAIN == UV_EAGAIN, "Need to translate error codes"); + STATIC_ASSERT(-ENOMEM == UV_ENOMEM, "Need to translate error codes"); +#endif + errno = 0; + if (error == UV_EINTR || error == UV_EAGAIN) { + continue; + } else if (error == UV_ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + continue; + } else { + return (ptrdiff_t)error; + } + } + } + return (ptrdiff_t)read_bytes; +} +#endif // HAVE_READV + +/// Write to a file +/// +/// @param[in] fd File descriptor to write to. +/// @param[in] buf Data to write. May be NULL if size is zero. +/// @param[in] size Amount of bytes to write. +/// +/// @return Number of bytes written or libuv error code (< 0). +ptrdiff_t os_write(const int fd, const char *const buf, const size_t size) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (buf == NULL) { + assert(size == 0); + return 0; + } + size_t written_bytes = 0; + while (written_bytes != size) { + const ptrdiff_t cur_written_bytes = write(fd, buf + written_bytes, + size - written_bytes); + if (cur_written_bytes > 0) { + written_bytes += (size_t)cur_written_bytes; + } + if (cur_written_bytes < 0) { +#ifdef HAVE_UV_TRANSLATE_SYS_ERROR + const int error = uv_translate_sys_error(errno); +#else + const int error = -errno; + STATIC_ASSERT(-EINTR == UV_EINTR, "Need to translate error codes"); + STATIC_ASSERT(-EAGAIN == UV_EAGAIN, "Need to translate error codes"); + // According to the man page open() may fail with ENOMEM, but write() + // can’t. +#endif + errno = 0; + if (error == UV_EINTR || error == UV_EAGAIN) { + continue; + } else { + return error; + } + } + if (cur_written_bytes == 0) { + return UV_UNKNOWN; + } + } + return (ptrdiff_t)written_bytes; +} + /// Flushes file modifications to disk. /// /// @param fd the file descriptor of the file to flush to disk. @@ -339,9 +542,8 @@ int os_open(const char* path, int flags, int mode) /// @return `0` on success, a libuv error code on failure. int os_fsync(int fd) { - uv_fs_t fsync_req; - int r = uv_fs_fsync(&fs_loop, &fsync_req, fd, NULL); - uv_fs_req_cleanup(&fsync_req); + int r; + RUN_UV_FS_FUNC(r, uv_fs_fsync, fd, NULL); return r; } @@ -379,16 +581,9 @@ int32_t os_getperm(const char_u *name) int os_setperm(const char_u *name, int perm) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_chmod(&fs_loop, &request, - (const char*)name, perm, NULL); - uv_fs_req_cleanup(&request); - - if (result == kLibuvSuccess) { - return OK; - } - - return FAIL; + int r; + RUN_UV_FS_FUNC(r, uv_fs_chmod, (const char *)name, perm, NULL); + return (r == kLibuvSuccess ? OK : FAIL); } /// Changes the ownership of the file referred to by the open file descriptor. @@ -397,13 +592,11 @@ int os_setperm(const char_u *name, int perm) /// /// @note If the `owner` or `group` is specified as `-1`, then that ID is not /// changed. -int os_fchown(int file_descriptor, uv_uid_t owner, uv_gid_t group) +int os_fchown(int fd, uv_uid_t owner, uv_gid_t group) { - uv_fs_t request; - int result = uv_fs_fchown(&fs_loop, &request, file_descriptor, - owner, group, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_fchown, fd, owner, group, NULL); + return r; } /// Check if a file exists. @@ -422,9 +615,8 @@ bool os_file_exists(const char_u *name) bool os_file_is_readable(const char *name) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - uv_fs_t req; - int r = uv_fs_access(&fs_loop, &req, name, R_OK, NULL); - uv_fs_req_cleanup(&req); + int r; + RUN_UV_FS_FUNC(r, uv_fs_access, name, R_OK, NULL); return (r == 0); } @@ -436,9 +628,8 @@ bool os_file_is_readable(const char *name) int os_file_is_writable(const char *name) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - uv_fs_t req; - int r = uv_fs_access(&fs_loop, &req, name, W_OK, NULL); - uv_fs_req_cleanup(&req); + int r; + RUN_UV_FS_FUNC(r, uv_fs_access, name, W_OK, NULL); if (r == 0) { return os_isdir((char_u *)name) ? 2 : 1; } @@ -451,16 +642,10 @@ int os_file_is_writable(const char *name) int os_rename(const char_u *path, const char_u *new_path) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_rename(&fs_loop, &request, - (const char *)path, (const char *)new_path, NULL); - uv_fs_req_cleanup(&request); - - if (result == kLibuvSuccess) { - return OK; - } - - return FAIL; + int r; + RUN_UV_FS_FUNC(r, uv_fs_rename, (const char *)path, (const char *)new_path, + NULL); + return (r == kLibuvSuccess ? OK : FAIL); } /// Make a directory. @@ -469,10 +654,9 @@ int os_rename(const char_u *path, const char_u *new_path) int os_mkdir(const char *path, int32_t mode) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_mkdir(&fs_loop, &request, path, mode, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_mkdir, path, mode, NULL); + return r; } /// Make a directory, with higher levels when needed @@ -554,10 +738,9 @@ int os_mkdtemp(const char *template, char *path) int os_rmdir(const char *path) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_rmdir(&fs_loop, &request, path, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_rmdir, path, NULL); + return r; } /// Opens a directory. @@ -599,10 +782,9 @@ void os_closedir(Directory *dir) int os_remove(const char *path) FUNC_ATTR_NONNULL_ALL { - uv_fs_t request; - int result = uv_fs_unlink(&fs_loop, &request, path, NULL); - uv_fs_req_cleanup(&request); - return result; + int r; + RUN_UV_FS_FUNC(r, uv_fs_unlink, path, NULL); + return r; } /// Get the file information for a given path diff --git a/src/nvim/rbuffer.c b/src/nvim/rbuffer.c index b3805a3a28..d4cc8c0d5d 100644 --- a/src/nvim/rbuffer.c +++ b/src/nvim/rbuffer.c @@ -153,7 +153,7 @@ void rbuffer_consumed(RBuffer *buf, size_t count) // Higher level functions for copying from/to RBuffer instances and data // pointers -size_t rbuffer_write(RBuffer *buf, char *src, size_t src_size) +size_t rbuffer_write(RBuffer *buf, const char *src, size_t src_size) FUNC_ATTR_NONNULL_ALL { size_t size = src_size; diff --git a/src/nvim/shada.c b/src/nvim/shada.c index 380d955f63..b5921eb810 100644 --- a/src/nvim/shada.c +++ b/src/nvim/shada.c @@ -5,7 +5,6 @@ #include <stdint.h> #include <inttypes.h> #include <errno.h> -#include <fcntl.h> #include <assert.h> #include <msgpack.h> @@ -36,6 +35,7 @@ #include "nvim/version.h" #include "nvim/path.h" #include "nvim/fileio.h" +#include "nvim/os/fileio.h" #include "nvim/strings.h" #include "nvim/quickfix.h" #include "nvim/eval/encode.h" @@ -409,7 +409,7 @@ typedef struct sd_read_def { ShaDaFileSkipper skip; ///< Function used to skip some bytes. void *cookie; ///< Data describing object read from. bool eof; ///< True if reader reached end of file. - char *error; ///< Error message in case of error. + const char *error; ///< Error message in case of error. uintmax_t fpos; ///< Current position (amount of bytes read since ///< reader structure initialization). May overflow. vimconv_T sd_conv; ///< Structure used for converting encodings of some @@ -433,7 +433,7 @@ typedef struct sd_write_def { ShaDaFileWriter write; ///< Writer function. ShaDaWriteCloser close; ///< Close function. void *cookie; ///< Data describing object written to. - char *error; ///< Error message in case of error. + const char *error; ///< Error message in case of error. vimconv_T sd_conv; ///< Structure used for converting encodings of some ///< items. } ShaDaWriteDef; @@ -666,38 +666,14 @@ static ptrdiff_t read_file(ShaDaReadDef *const sd_reader, void *const dest, const size_t size) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - size_t read_bytes = 0; - bool did_try_to_free = false; - const int fd = (int)(intptr_t) sd_reader->cookie; - while (read_bytes != size) { - const ptrdiff_t cur_read_bytes = read(fd, ((char *) dest) + read_bytes, - size - read_bytes); - if (cur_read_bytes > 0) { - read_bytes += (size_t) cur_read_bytes; - sd_reader->fpos += (uintmax_t) cur_read_bytes; - assert(read_bytes <= size); - } - if (cur_read_bytes < 0) { - if (errno == EINTR || errno == EAGAIN) { - errno = 0; - continue; - } else if (errno == ENOMEM && !did_try_to_free) { - try_to_free_memory(); - did_try_to_free = true; - errno = 0; - continue; - } else { - sd_reader->error = strerror(errno); - errno = 0; - return -1; - } - } - if (cur_read_bytes == 0) { - sd_reader->eof = true; - break; - } + const ptrdiff_t ret = file_read(sd_reader->cookie, dest, size); + sd_reader->eof = file_eof(sd_reader->cookie); + if (ret < 0) { + sd_reader->error = os_strerror((int)ret); + return -1; } - return (ptrdiff_t) read_bytes; + sd_reader->fpos += (size_t)ret; + return ret; } /// Read one character @@ -720,50 +696,26 @@ static ptrdiff_t write_file(ShaDaWriteDef *const sd_writer, const size_t size) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - size_t written_bytes = 0; - const int fd = (int)(intptr_t) sd_writer->cookie; - while (written_bytes != size) { - const ptrdiff_t cur_written_bytes = write(fd, (char *) dest + written_bytes, - size - written_bytes); - if (cur_written_bytes > 0) { - written_bytes += (size_t) cur_written_bytes; - } - if (cur_written_bytes < 0) { - if (errno == EINTR || errno == EAGAIN) { - errno = 0; - continue; - } else { - sd_writer->error = strerror(errno); - errno = 0; - return -1; - } - } - if (cur_written_bytes == 0) { - sd_writer->error = "Zero bytes written."; - return -1; - } + const ptrdiff_t ret = file_write(sd_writer->cookie, dest, size); + if (ret < 0) { + sd_writer->error = os_strerror((int)ret); + return -1; } - return (ptrdiff_t) written_bytes; + return ret; } /// Wrapper for closing file descriptors opened for reading static void close_sd_reader(ShaDaReadDef *const sd_reader) FUNC_ATTR_NONNULL_ALL { - close_file((int)(intptr_t) sd_reader->cookie); + close_file(sd_reader->cookie); } /// Wrapper for closing file descriptors opened for writing static void close_sd_writer(ShaDaWriteDef *const sd_writer) FUNC_ATTR_NONNULL_ALL { - const int fd = (int)(intptr_t) sd_writer->cookie; - if (os_fsync(fd) < 0) { - emsgf(_(SERR "System error while synchronizing ShaDa file: %s"), - os_strerror(errno)); - errno = 0; - } - close_file(fd); + close_file(sd_writer->cookie); } /// Wrapper for read that reads to IObuff and ignores bytes read @@ -779,19 +731,20 @@ static int sd_reader_skip_read(ShaDaReadDef *const sd_reader, const size_t offset) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - size_t read_bytes = 0; - do { - ptrdiff_t new_read_bytes = sd_reader->read( - sd_reader, IObuff, (size_t) (offset - read_bytes > IOSIZE - ? IOSIZE - : offset - read_bytes)); - if (new_read_bytes == -1) { - return FAIL; + const ptrdiff_t skip_bytes = file_skip(sd_reader->cookie, offset); + if (skip_bytes < 0) { + sd_reader->error = os_strerror((int)skip_bytes); + return FAIL; + } else if (skip_bytes != (ptrdiff_t)offset) { + assert(skip_bytes < (ptrdiff_t)offset); + sd_reader->eof = file_eof(sd_reader->cookie); + if (!sd_reader->eof) { + sd_reader->error = _("too few bytes read"); } - read_bytes += (size_t) new_read_bytes; - } while (read_bytes < offset && !sd_reader->eof); - - return (read_bytes == offset ? OK : FAIL); + return FAIL; + } + sd_reader->fpos += (size_t)skip_bytes; + return OK; } /// Wrapper for read that can be used when lseek cannot be used @@ -824,37 +777,6 @@ static ShaDaReadResult sd_reader_skip(ShaDaReadDef *const sd_reader, return kSDReadStatusSuccess; } -/// Wrapper for opening file descriptors -/// -/// All arguments are passed to os_open(). -/// -/// @return file descriptor or libuv error on failure. -static int open_file(const char *const fname, const int flags, const int mode) - FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL -{ - bool did_try_to_free = false; - int fd; -open_file_start: - fd = os_open(fname, flags, mode); - - if (fd < 0) { - if (fd == UV_ENOENT) { - return fd; - } - if (fd == UV_ENOMEM && !did_try_to_free) { - try_to_free_memory(); - did_try_to_free = true; - goto open_file_start; - } - if (fd != UV_EEXIST) { - emsgf(_(SERR "System error while opening ShaDa file %s: %s"), - fname, os_strerror(fd)); - } - return fd; - } - return fd; -} - /// Open ShaDa file for reading /// /// @param[in] fname File name to open. @@ -865,11 +787,7 @@ static int open_shada_file_for_reading(const char *const fname, ShaDaReadDef *sd_reader) FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL { - const intptr_t fd = (intptr_t) open_file(fname, O_RDONLY, 0); - - if (fd < 0) { - return (int) fd; - } + int error; *sd_reader = (ShaDaReadDef) { .read = &read_file, @@ -878,8 +796,11 @@ static int open_shada_file_for_reading(const char *const fname, .error = NULL, .eof = false, .fpos = 0, - .cookie = (void *) fd, + .cookie = file_open_new(&error, fname, kFileReadOnly, 0), }; + if (sd_reader->cookie == NULL) { + return error; + } convert_setup(&sd_reader->sd_conv, "utf-8", p_enc); @@ -887,18 +808,12 @@ static int open_shada_file_for_reading(const char *const fname, } /// Wrapper for closing file descriptors -static void close_file(int fd) +static void close_file(void *cookie) { -close_file_start: - if (close(fd) == -1) { - if (errno == EINTR) { - errno = 0; - goto close_file_start; - } else { - emsgf(_(SERR "System error while closing ShaDa file: %s"), - strerror(errno)); - errno = 0; - } + const int error = file_free(cookie); + if (error != 0) { + emsgf(_(SERR "System error while closing ShaDa file: %s"), + os_strerror(error)); } } @@ -978,7 +893,7 @@ static int shada_read_file(const char *const file, const int flags) } if (of_ret != 0) { - if (of_ret == UV_ENOENT && (flags & kShaDaMissingError)) { + if (of_ret != UV_ENOENT || (flags & kShaDaMissingError)) { emsgf(_(SERR "System error while opening ShaDa file %s for reading: %s"), fname, os_strerror(of_ret)); } @@ -1421,7 +1336,7 @@ static void shada_read(ShaDaReadDef *const sd_reader, const int flags) } } if (!op_register_set(cur_entry.data.reg.name, (yankreg_T) { - .y_array = (char_u **) cur_entry.data.reg.contents, + .y_array = (char_u **)cur_entry.data.reg.contents, .y_size = cur_entry.data.reg.contents_size, .y_type = cur_entry.data.reg.type, .y_width = (colnr_T) cur_entry.data.reg.width, @@ -2968,17 +2883,23 @@ int shada_write_file(const char *const file, bool nomerge) char *const fname = shada_filename(file); char *tempname = NULL; - ShaDaWriteDef sd_writer = (ShaDaWriteDef) { + ShaDaWriteDef sd_writer = { .write = &write_file, .close = &close_sd_writer, .error = NULL, }; - ShaDaReadDef sd_reader; - - intptr_t fd; + ShaDaReadDef sd_reader = { .close = NULL }; if (!nomerge) { - if (open_shada_file_for_reading(fname, &sd_reader) != 0) { + int error; + if ((error = open_shada_file_for_reading(fname, &sd_reader)) != 0) { + if (error != UV_ENOENT) { + emsgf(_(SERR "System error while opening ShaDa file %s for reading " + "to merge before writing it: %s"), + fname, os_strerror(error)); + // Try writing the file even if opening it emerged any issues besides + // file not existing: maybe writing will succeed nevertheless. + } nomerge = true; goto shada_write_file_nomerge; } @@ -2996,15 +2917,11 @@ int shada_write_file(const char *const file, bool nomerge) // 2: Make sure that user can always read and write the result. // 3: If somebody happened to delete the file after it was opened for // reading use u=rw permissions. -shada_write_file_open: - fd = (intptr_t) open_file(tempname, O_CREAT|O_WRONLY|O_NOFOLLOW|O_EXCL, - perm); - if (fd < 0) { - if (fd == UV_EEXIST -#ifdef ELOOP - || fd == UV_ELOOP -#endif - ) { +shada_write_file_open: {} + sd_writer.cookie = file_open_new( + &error, tempname, kFileCreateOnly|kFileNoSymlink, perm); + if (sd_writer.cookie == NULL) { + if (error == UV_EEXIST || error == UV_ELOOP) { // File already exists, try another name char *const wp = tempname + strlen(tempname) - 1; if (*wp == 'z') { @@ -3014,11 +2931,16 @@ shada_write_file_open: fname); xfree(fname); xfree(tempname); + assert(sd_reader.close != NULL); + sd_reader.close(&sd_reader); return FAIL; } else { (*wp)++; goto shada_write_file_open; } + } else { + emsgf(_(SERR "System error while opening temporary ShaDa file %s " + "for writing: %s"), tempname, os_strerror(error)); } } } @@ -3042,23 +2964,29 @@ shada_write_file_nomerge: {} } *tail = tail_save; } - fd = (intptr_t) open_file(fname, O_CREAT|O_WRONLY|O_TRUNC, - 0600); - } - - if (p_verbose > 0) { - verbose_enter(); - smsg(_("Writing ShaDa file \"%s\""), fname); - verbose_leave(); + int error; + sd_writer.cookie = file_open_new(&error, fname, kFileCreate|kFileTruncate, + 0600); + if (sd_writer.cookie == NULL) { + emsgf(_(SERR "System error while opening ShaDa file %s for writing: %s"), + fname, os_strerror(error)); + } } - if (fd < 0) { + if (sd_writer.cookie == NULL) { xfree(fname); xfree(tempname); + if (sd_reader.close != NULL) { + sd_reader.close(&sd_reader); + } return FAIL; } - sd_writer.cookie = (void *) fd; + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Writing ShaDa file \"%s\""), fname); + verbose_leave(); + } convert_setup(&sd_writer.sd_conv, p_enc, "utf-8"); @@ -3066,15 +2994,11 @@ shada_write_file_nomerge: {} ? NULL : &sd_reader)); assert(sw_ret != kSDWriteIgnError); -#ifndef UNIX - sd_writer.close(&sd_writer); -#endif if (!nomerge) { sd_reader.close(&sd_reader); bool did_remove = false; if (sw_ret == kSDWriteSuccessfull) { #ifdef UNIX - bool closed = false; // For Unix we check the owner of the file. It's not very nice to // overwrite a user’s viminfo file after a "su root", with a // viminfo file that the user can't read. @@ -3083,16 +3007,15 @@ shada_write_file_nomerge: {} if (getuid() == ROOT_UID) { if (old_info.stat.st_uid != ROOT_UID || old_info.stat.st_gid != getgid()) { - const uv_uid_t old_uid = (uv_uid_t) old_info.stat.st_uid; - const uv_gid_t old_gid = (uv_gid_t) old_info.stat.st_gid; - const int fchown_ret = os_fchown((int) fd, old_uid, old_gid); - sd_writer.close(&sd_writer); + const uv_uid_t old_uid = (uv_uid_t)old_info.stat.st_uid; + const uv_gid_t old_gid = (uv_gid_t)old_info.stat.st_gid; + const int fchown_ret = os_fchown(file_fd(sd_writer.cookie), + old_uid, old_gid); if (fchown_ret != 0) { EMSG3(_(RNERR "Failed setting uid and gid for file %s: %s"), tempname, os_strerror(fchown_ret)); goto shada_write_file_did_not_remove; } - closed = true; } } else if (!(old_info.stat.st_uid == getuid() ? (old_info.stat.st_mode & 0200) @@ -3100,13 +3023,9 @@ shada_write_file_nomerge: {} ? (old_info.stat.st_mode & 0020) : (old_info.stat.st_mode & 0002)))) { EMSG2(_("E137: ShaDa file is not writable: %s"), fname); - sd_writer.close(&sd_writer); goto shada_write_file_did_not_remove; } } - if (!closed) { - sd_writer.close(&sd_writer); - } #endif if (vim_rename(tempname, fname) == -1) { EMSG3(_(RNERR "Can't rename ShaDa file from %s to %s!"), @@ -3133,6 +3052,7 @@ shada_write_file_did_not_remove: } xfree(tempname); } + sd_writer.close(&sd_writer); xfree(fname); return OK; @@ -3262,20 +3182,20 @@ static ShaDaReadResult fread_len(ShaDaReadDef *const sd_reader, FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { const ptrdiff_t read_bytes = sd_reader->read(sd_reader, buffer, length); - (void) read_bytes; - if (sd_reader->error != NULL) { - emsgf(_(SERR "System error while reading ShaDa file: %s"), - sd_reader->error); - return kSDReadStatusReadError; - } else if (sd_reader->eof) { - emsgf(_(RCERR "Error while reading ShaDa file: " - "last entry specified that it occupies %" PRIu64 " bytes, " - "but file ended earlier"), - (uint64_t) length); - return kSDReadStatusNotShaDa; + if (read_bytes != (ptrdiff_t)length) { + if (sd_reader->error != NULL) { + emsgf(_(SERR "System error while reading ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else { + emsgf(_(RCERR "Error while reading ShaDa file: " + "last entry specified that it occupies %" PRIu64 " bytes, " + "but file ended earlier"), + (uint64_t)length); + return kSDReadStatusNotShaDa; + } } - assert(read_bytes >= 0 && (size_t) read_bytes == length); return kSDReadStatusSuccess; } diff --git a/test/functional/api/server_notifications_spec.lua b/test/functional/api/server_notifications_spec.lua index d68ce411c0..88e8c60560 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,33 @@ 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 = 1000 + meths.command(('call map(range(%u), "extend(g:, {\'l\': [g:l]})")'):format(nest_level - 1)) + eval('rpcnotify('..channel..', "event", g:l)') + local msg = next_message() + eq('notification', msg[1]) + eq('event', msg[2]) + local act_ret = msg[3] + local act_nest_level = 0 + while act_ret do + if type(act_ret) == 'table' then + local cur_act_ret = nil + for k, v in pairs(act_ret) do + eq(1, k) + cur_act_ret = v + end + if cur_act_ret then + act_nest_level = act_nest_level + 1 + end + act_ret = cur_act_ret + else + eq(nil, act_ret) + end + end + eq(nest_level, act_nest_level) + end) end) end) diff --git a/test/unit/api/helpers.lua b/test/unit/api/helpers.lua new file mode 100644 index 0000000000..883e1c6c19 --- /dev/null +++ b/test/unit/api/helpers.lua @@ -0,0 +1,156 @@ +local helpers = require('test.unit.helpers') +local eval_helpers = require('test.unit.eval.helpers') + +local cimport = helpers.cimport +local to_cstr = helpers.to_cstr +local ffi = helpers.ffi + +local list_type = eval_helpers.list_type +local dict_type = eval_helpers.dict_type +local func_type = eval_helpers.func_type +local nil_value = eval_helpers.nil_value +local int_type = eval_helpers.int_type +local flt_type = eval_helpers.flt_type +local type_key = eval_helpers.type_key + +local api = cimport('./src/nvim/api/private/defs.h', + './src/nvim/api/private/helpers.h', + './src/nvim/memory.h') + +local obj2lua + +local obj2lua_tab = { + [tonumber(api.kObjectTypeArray)] = function(obj) + local ret = {[type_key]=list_type} + for i = 1,tonumber(obj.data.array.size) do + ret[i] = obj2lua(obj.data.array.items[i - 1]) + end + if ret[1] then + ret[type_key] = nil + end + return ret + end, + [tonumber(api.kObjectTypeDictionary)] = function(obj) + local ret = {} + for i = 1,tonumber(obj.data.dictionary.size) do + local kv_pair = obj.data.dictionary.items[i - 1] + ret[ffi.string(kv_pair.key.data, kv_pair.key.size)] = obj2lua(kv_pair.value) + end + return ret + end, + [tonumber(api.kObjectTypeBoolean)] = function(obj) + if obj.data.boolean == false then + return false + else + return true + end + end, + [tonumber(api.kObjectTypeNil)] = function(_) + return nil_value + end, + [tonumber(api.kObjectTypeFloat)] = function(obj) + return tonumber(obj.data.floating) + end, + [tonumber(api.kObjectTypeInteger)] = function(obj) + return {[type_key]=int_type, value=tonumber(obj.data.integer)} + end, + [tonumber(api.kObjectTypeString)] = function(obj) + return ffi.string(obj.data.string.data, obj.data.string.size) + end, +} + +obj2lua = function(obj) + return ((obj2lua_tab[tonumber(obj['type'])] or function(obj_inner) + assert(false, 'Converting ' .. tostring(tonumber(obj_inner['type'])) .. ' is not implementing yet') + end)(obj)) +end + +local obj = function(typ, data) + return ffi.gc(ffi.new('Object', {['type']=typ, data=data}), + api.api_free_object) +end + +local lua2obj + +local lua2obj_type_tab = { + [int_type] = function(l) + return obj(api.kObjectTypeInteger, {integer=l.value}) + end, + [flt_type] = function(l) + return obj(api.kObjectTypeFloat, {floating=l}) + end, + [list_type] = function(l) + local len = #l + local arr = obj(api.kObjectTypeArray, {array={ + size=len, + capacity=len, + items=ffi.cast('Object *', api.xmalloc(len * ffi.sizeof('Object'))), + }}) + for i = 1, len do + arr.data.array.items[i - 1] = ffi.gc(lua2obj(l[i]), nil) + end + return arr + end, + [dict_type] = function(l) + local kvs = {} + for k, v in pairs(l) do + if type(k) == 'string' then + kvs[#kvs + 1] = {k, v} + end + end + local len = #kvs + local dct = obj(api.kObjectTypeDictionary, {dictionary={ + size=len, + capacity=len, + items=ffi.cast('KeyValuePair *', + api.xmalloc(len * ffi.sizeof('KeyValuePair'))), + }}) + for i = 1, len do + local key, val = table.unpack(kvs[i]) + dct.data.dictionary.items[i - 1] = ffi.new( + 'KeyValuePair', {key=ffi.gc(lua2obj(key), nil).data.string, + value=ffi.gc(lua2obj(val), nil)}) + end + return dct + end, +} + +lua2obj = function(l) + if type(l) == 'table' then + if l[type_key] then + return lua2obj_type_tab[l[type_key]](l) + else + if l[1] then + return lua2obj_type_tab[list_type](l) + else + return lua2obj_type_tab[dict_type](l) + end + end + elseif type(l) == 'number' then + return lua2obj_type_tab[flt_type](l) + elseif type(l) == 'boolean' then + return obj(api.kObjectTypeBoolean, {boolean=l}) + elseif type(l) == 'string' then + return obj(api.kObjectTypeString, {string={ + size=#l, + data=api.xmemdupz(to_cstr(l), #l), + }}) + elseif l == nil or l == nil_value then + return obj(api.kObjectTypeNil, {integer=0}) + end +end + +return { + list_type=list_type, + dict_type=dict_type, + func_type=func_type, + int_type=int_type, + flt_type=flt_type, + + nil_value=nil_value, + + type_key=type_key, + + obj2lua=obj2lua, + lua2obj=lua2obj, +} diff --git a/test/unit/api/private_helpers_spec.lua b/test/unit/api/private_helpers_spec.lua new file mode 100644 index 0000000000..1d7c03787b --- /dev/null +++ b/test/unit/api/private_helpers_spec.lua @@ -0,0 +1,88 @@ +local helpers = require('test.unit.helpers') +local eval_helpers = require('test.unit.eval.helpers') +local api_helpers = require('test.unit.api.helpers') + +local cimport = helpers.cimport +local NULL = helpers.NULL +local eq = helpers.eq + +local lua2typvalt = eval_helpers.lua2typvalt +local typvalt = eval_helpers.typvalt + +local nil_value = api_helpers.nil_value +local list_type = api_helpers.list_type +local int_type = api_helpers.int_type +local type_key = api_helpers.type_key +local obj2lua = api_helpers.obj2lua + +local api = cimport('./src/nvim/api/private/helpers.h') + +describe('vim_to_object', function() + local vim_to_object = function(l) + return obj2lua(api.vim_to_object(lua2typvalt(l))) + end + + local different_output_test = function(name, input, output) + it(name, function() + eq(output, vim_to_object(input)) + end) + end + + local simple_test = function(name, l) + different_output_test(name, l, l) + end + + simple_test('converts true', true) + simple_test('converts false', false) + simple_test('converts nil', nil_value) + simple_test('converts 1', 1) + simple_test('converts -1.5', -1.5) + simple_test('converts empty string', '') + simple_test('converts non-empty string', 'foobar') + simple_test('converts integer 10', {[type_key]=int_type, value=10}) + simple_test('converts empty dictionary', {}) + simple_test('converts dictionary with scalar values', {test=10, test2=true, test3='test'}) + simple_test('converts dictionary with containers inside', {test={}, test2={1, 2}}) + simple_test('converts empty list', {[type_key]=list_type}) + simple_test('converts list with scalar values', {1, 2, 'test', 'foo'}) + simple_test('converts list with containers inside', {{}, {test={}, test3={test4=true}}}) + + local dct = {} + dct.dct = dct + different_output_test('outputs nil for nested dictionaries (1 level)', dct, {dct=nil_value}) + + local lst = {} + lst[1] = lst + different_output_test('outputs nil for nested lists (1 level)', lst, {nil_value}) + + local dct2 = {test=true, dict=nil_value} + dct2.dct = {dct2} + different_output_test('outputs nil for nested dictionaries (2 level, in list)', + dct2, {dct={nil_value}, test=true, dict=nil_value}) + + local dct3 = {test=true, dict=nil_value} + dct3.dct = {dctin=dct3} + different_output_test('outputs nil for nested dictionaries (2 level, in dict)', + dct3, {dct={dctin=nil_value}, test=true, dict=nil_value}) + + local lst2 = {} + lst2[1] = {lst2} + different_output_test('outputs nil for nested lists (2 level, in list)', lst2, {{nil_value}}) + + local lst3 = {nil, true, false, 'ttest'} + lst3[1] = {lst=lst3} + different_output_test('outputs nil for nested lists (2 level, in dict)', + lst3, {{lst=nil_value}, true, false, 'ttest'}) + + it('outputs empty list for NULL list', function() + local tt = typvalt('VAR_LIST', {v_list=NULL}) + eq(nil, tt.vval.v_list) + eq({[type_key]=list_type}, obj2lua(api.vim_to_object(tt))) + end) + + it('outputs empty dict for NULL dict', function() + local tt = typvalt('VAR_DICT', {v_dict=NULL}) + eq(nil, tt.vval.v_dict) + eq({}, obj2lua(api.vim_to_object(tt))) + end) +end) diff --git a/test/unit/eval/encode_spec.lua b/test/unit/eval/encode_spec.lua index f151a191fb..98fc8305e0 100644 --- a/test/unit/eval/encode_spec.lua +++ b/test/unit/eval/encode_spec.lua @@ -27,74 +27,74 @@ describe('encode_list_write()', function() it('writes ASCII string literal with printable characters', function() local l = list() eq(0, encode_list_write(l, 'abc')) - eq({[type_key]=list_type, 'abc'}, lst2tbl(l)) + eq({'abc'}, lst2tbl(l)) end) it('writes string starting with NL', function() local l = list() eq(0, encode_list_write(l, '\nabc')) - eq({[type_key]=list_type, null_string, 'abc'}, lst2tbl(l)) + eq({null_string, 'abc'}, lst2tbl(l)) end) it('writes string starting with NL twice', function() local l = list() eq(0, encode_list_write(l, '\nabc')) - eq({[type_key]=list_type, null_string, 'abc'}, lst2tbl(l)) + eq({null_string, 'abc'}, lst2tbl(l)) eq(0, encode_list_write(l, '\nabc')) - eq({[type_key]=list_type, null_string, 'abc', 'abc'}, lst2tbl(l)) + eq({null_string, 'abc', 'abc'}, lst2tbl(l)) end) it('writes string ending with NL', function() local l = list() eq(0, encode_list_write(l, 'abc\n')) - eq({[type_key]=list_type, 'abc', null_string}, lst2tbl(l)) + eq({'abc', null_string}, lst2tbl(l)) end) it('writes string ending with NL twice', function() local l = list() eq(0, encode_list_write(l, 'abc\n')) - eq({[type_key]=list_type, 'abc', null_string}, lst2tbl(l)) + eq({'abc', null_string}, lst2tbl(l)) eq(0, encode_list_write(l, 'abc\n')) - eq({[type_key]=list_type, 'abc', 'abc', null_string}, lst2tbl(l)) + eq({'abc', 'abc', null_string}, lst2tbl(l)) end) it('writes string starting, ending and containing NL twice', function() local l = list() eq(0, encode_list_write(l, '\na\nb\n')) - eq({[type_key]=list_type, null_string, 'a', 'b', null_string}, lst2tbl(l)) + eq({null_string, 'a', 'b', null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\na\nb\n')) - eq({[type_key]=list_type, null_string, 'a', 'b', null_string, 'a', 'b', null_string}, lst2tbl(l)) + eq({null_string, 'a', 'b', null_string, 'a', 'b', null_string}, lst2tbl(l)) end) it('writes string starting, ending and containing NUL with NL between twice', function() local l = list() eq(0, encode_list_write(l, '\0\n\0\n\0')) - eq({[type_key]=list_type, '\n', '\n', '\n'}, lst2tbl(l)) + eq({'\n', '\n', '\n'}, lst2tbl(l)) eq(0, encode_list_write(l, '\0\n\0\n\0')) - eq({[type_key]=list_type, '\n', '\n', '\n\n', '\n', '\n'}, lst2tbl(l)) + eq({'\n', '\n', '\n\n', '\n', '\n'}, lst2tbl(l)) end) it('writes string starting, ending and containing NL with NUL between twice', function() local l = list() eq(0, encode_list_write(l, '\n\0\n\0\n')) - eq({[type_key]=list_type, null_string, '\n', '\n', null_string}, lst2tbl(l)) + eq({null_string, '\n', '\n', null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\n\0\n\0\n')) - eq({[type_key]=list_type, null_string, '\n', '\n', null_string, '\n', '\n', null_string}, lst2tbl(l)) + eq({null_string, '\n', '\n', null_string, '\n', '\n', null_string}, lst2tbl(l)) end) it('writes string containing a single NL twice', function() local l = list() eq(0, encode_list_write(l, '\n')) - eq({[type_key]=list_type, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\n')) - eq({[type_key]=list_type, null_string, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string, null_string}, lst2tbl(l)) end) it('writes string containing a few NLs twice', function() local l = list() eq(0, encode_list_write(l, '\n\n\n')) - eq({[type_key]=list_type, null_string, null_string, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string, null_string, null_string}, lst2tbl(l)) eq(0, encode_list_write(l, '\n\n\n')) - eq({[type_key]=list_type, null_string, null_string, null_string, null_string, null_string, null_string, null_string}, lst2tbl(l)) + eq({null_string, null_string, null_string, null_string, null_string, null_string, null_string}, lst2tbl(l)) end) end) diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua index 2367f03e0d..45fbf8da5c 100644 --- a/test/unit/eval/helpers.lua +++ b/test/unit/eval/helpers.lua @@ -11,6 +11,12 @@ local null_string = {[true]='NULL string'} local null_list = {[true]='NULL list'} local type_key = {[true]='type key'} local list_type = {[true]='list type'} +local dict_type = {[true]='dict type'} +local func_type = {[true]='func type'} +local int_type = {[true]='int type'} +local flt_type = {[true]='flt type'} + +local nil_value = {[true]='nil'} local function list(...) local ret = ffi.gc(eval.list_alloc(), eval.list_unref) @@ -37,7 +43,53 @@ local function list(...) return ret end -local lst2tbl = function(l) +local special_tab = { + [eval.kSpecialVarFalse] = false, + [eval.kSpecialVarNull] = nil_value, + [eval.kSpecialVarTrue] = true, +} + +local lst2tbl +local dct2tbl + +local typvalt2lua_tab + +typvalt2lua_tab = { + [tonumber(eval.VAR_SPECIAL)] = function(t) + return special_tab[t.vval.v_special] + end, + [tonumber(eval.VAR_NUMBER)] = function(t) + return {[type_key]=int_type, value=tonumber(t.vval.v_number)} + end, + [tonumber(eval.VAR_FLOAT)] = function(t) + return tonumber(t.vval.v_float) + end, + [tonumber(eval.VAR_STRING)] = function(t) + local str = t.vval.v_string + if str == nil then + return null_string + else + return ffi.string(str) + end + end, + [tonumber(eval.VAR_LIST)] = function(t) + return lst2tbl(t.vval.v_list) + end, + [tonumber(eval.VAR_DICT)] = function(t) + return dct2tbl(t.vval.v_dict) + end, + [tonumber(eval.VAR_FUNC)] = function(t) + return {[type_key]=func_type, value=typvalt2lua_tab[eval.VAR_STRING](t)} + end, +} + +local typvalt2lua = function(t) + return ((typvalt2lua_tab[tonumber(t.v_type)] or function(t_inner) + assert(false, 'Converting ' .. tonumber(t_inner.v_type) .. ' was not implemented yet') + end)(t)) +end + +lst2tbl = function(l) local ret = {[type_key]=list_type} if l == nil then return ret @@ -45,28 +97,119 @@ local lst2tbl = function(l) local li = l.lv_first -- (listitem_T *) NULL is equal to nil, but yet it is not false. while li ~= nil do - local typ = li.li_tv.v_type - if typ == eval.VAR_STRING then - local str = li.li_tv.vval.v_string - if str == nil then - ret[#ret + 1] = null_string - else - ret[#ret + 1] = ffi.string(str) + ret[#ret + 1] = typvalt2lua(li.li_tv) + li = li.li_next + end + if ret[1] then + ret[type_key] = nil + end + return ret +end + +dct2tbl = function(d) + local ret = {d=d} + assert(false, 'Converting dictionaries is not implemented yet') + return ret +end + +local lua2typvalt + +local typvalt = function(typ, vval) + if type(typ) == 'string' then + typ = eval[typ] + end + return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.clear_tv) +end + +local lua2typvalt_type_tab = { + [int_type] = function(l, _) + return typvalt(eval.VAR_NUMBER, {v_number=l.value}) + end, + [flt_type] = function(l, processed) + return lua2typvalt(l.value, processed) + end, + [list_type] = function(l, processed) + if processed[l] then + processed[l].lv_refcount = processed[l].lv_refcount + 1 + return typvalt(eval.VAR_LIST, {v_list=processed[l]}) + end + local lst = eval.list_alloc() + lst.lv_refcount = 1 + processed[l] = lst + local ret = typvalt(eval.VAR_LIST, {v_list=lst}) + for i = 1, #l do + local item_tv = ffi.gc(lua2typvalt(l[i], processed), nil) + eval.list_append_tv(lst, item_tv) + eval.clear_tv(item_tv) + end + return ret + end, + [dict_type] = function(l, processed) + if processed[l] then + processed[l].dv_refcount = processed[l].dv_refcount + 1 + return typvalt(eval.VAR_DICT, {v_dict=processed[l]}) + end + local dct = eval.dict_alloc() + dct.dv_refcount = 1 + processed[l] = dct + local ret = typvalt(eval.VAR_DICT, {v_dict=dct}) + for k, v in pairs(l) do + if type(k) == 'string' then + local di = eval.dictitem_alloc(to_cstr(k)) + local val_tv = ffi.gc(lua2typvalt(v, processed), nil) + eval.copy_tv(val_tv, di.di_tv) + eval.clear_tv(val_tv) + eval.dict_add(dct, di) end + end + return ret + end, +} + +lua2typvalt = function(l, processed) + processed = processed or {} + if l == nil or l == nil_value then + return typvalt(eval.VAR_SPECIAL, {v_special=eval.kSpecialVarNull}) + elseif type(l) == 'table' then + if l[type_key] then + return lua2typvalt_type_tab[l[type_key]](l, processed) else - assert(false, 'Not implemented yet') + if l[1] then + return lua2typvalt_type_tab[list_type](l, processed) + else + return lua2typvalt_type_tab[dict_type](l, processed) + end end - li = li.li_next + elseif type(l) == 'number' then + return typvalt(eval.VAR_FLOAT, {v_float=l}) + elseif type(l) == 'boolean' then + return typvalt(eval.VAR_SPECIAL, { + v_special=(l and eval.kSpecialVarTrue or eval.kSpecialVarFalse) + }) + elseif type(l) == 'string' then + return typvalt(eval.VAR_STRING, {v_string=eval.xmemdupz(to_cstr(l), #l)}) end - return ret end return { null_string=null_string, null_list=null_list, list_type=list_type, + dict_type=dict_type, + func_type=func_type, + int_type=int_type, + flt_type=flt_type, + + nil_value=nil_value, + type_key=type_key, list=list, lst2tbl=lst2tbl, + dct2tbl=dct2tbl, + + lua2typvalt=lua2typvalt, + typvalt2lua=typvalt2lua, + + typvalt=typvalt, } diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index f0c193884e..91da459393 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -7,6 +7,7 @@ local global_helpers = require('test.helpers') local neq = global_helpers.neq local eq = global_helpers.eq +local ok = global_helpers.ok -- add some standard header locations for _, p in ipairs(Paths.include_paths) do @@ -34,7 +35,8 @@ local function filter_complex_blocks(body) if not (string.find(line, "(^)", 1, true) ~= nil or string.find(line, "_ISwupper", 1, true) or string.find(line, "msgpack_zone_push_finalizer") - or string.find(line, "msgpack_unpacker_reserve_buffer")) then + or string.find(line, "msgpack_unpacker_reserve_buffer") + or string.find(line, "inline _Bool")) then result[#result + 1] = line end end @@ -156,6 +158,7 @@ return { cimport = cimport, cppimport = cppimport, internalize = internalize, + ok = ok, eq = eq, neq = neq, ffi = ffi, diff --git a/test/unit/os/fileio_spec.lua b/test/unit/os/fileio_spec.lua new file mode 100644 index 0000000000..5358022422 --- /dev/null +++ b/test/unit/os/fileio_spec.lua @@ -0,0 +1,365 @@ +local lfs = require('lfs') + +local helpers = require('test.unit.helpers') + +local eq = helpers.eq +local ffi = helpers.ffi +local cimport = helpers.cimport + +local m = cimport('./src/nvim/os/fileio.h') + +local fcontents = '' +for i = 0, 255 do + fcontents = fcontents .. (i == 0 and '\0' or ('%c'):format(i)) +end +fcontents = fcontents:rep(16) + +local dir = 'Xtest-unit-file_spec.d' +local file1 = dir .. '/file1.dat' +local file2 = dir .. '/file2.dat' +local linkf = dir .. '/file.lnk' +local linkb = dir .. '/broken.lnk' +local filec = dir .. '/created-file.dat' + +before_each(function() + lfs.mkdir(dir); + + local f1 = io.open(file1, 'w') + f1:write(fcontents) + f1:close() + + local f2 = io.open(file2, 'w') + f2:write(fcontents) + f2:close() + + lfs.link('file1.dat', linkf, true) + lfs.link('broken.dat', linkb, true) +end) + +after_each(function() + os.remove(file1) + os.remove(file2) + os.remove(linkf) + os.remove(linkb) + os.remove(filec) + lfs.rmdir(dir) +end) + +local function file_open(fname, flags, mode) + local ret2 = ffi.new('FileDescriptor') + local ret1 = m.file_open(ret2, fname, flags, mode) + return ret1, ret2 +end + +local function file_open_new(fname, flags, mode) + local ret1 = ffi.new('int[?]', 1, {0}) + local ret2 = ffi.gc(m.file_open_new(ret1, fname, flags, mode), nil) + return ret1[0], ret2 +end + +local function file_write(fp, buf) + return m.file_write(fp, buf, #buf) +end + +local function file_read(fp, size) + local buf = nil + if size == nil then + size = 0 + else + -- For some reason if length of NUL-bytes-string is the same as `char[?]` + -- size luajit garbage collector crashes. But it does not do so in + -- os_read[v] tests in os/fs_spec.lua. + buf = ffi.new('char[?]', size + 1, ('\0'):rep(size)) + end + local ret1 = m.file_read(fp, buf, size) + local ret2 = '' + if buf ~= nil then + ret2 = ffi.string(buf, size) + end + return ret1, ret2 +end + +local function file_fsync(fp) + return m.file_fsync(fp) +end + +local function file_skip(fp, size) + return m.file_skip(fp, size) +end + +describe('file_open', function() + it('can create a rwx------ file with kFileCreate', function() + local err, fp = file_open(filec, m.kFileCreate, 448) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rwx------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('can create a rw------- file with kFileCreate', function() + local err, fp = file_open(filec, m.kFileCreate, 384) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rw-------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('can create a rwx------ file with kFileCreateOnly', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 448) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rwx------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('can create a rw------- file with kFileCreateOnly', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + local attrs = lfs.attributes(filec) + eq('rw-------', attrs.permissions) + eq(0, m.file_close(fp)) + end) + + it('fails to open an existing file with kFileCreateOnly', function() + local err, _ = file_open(file1, m.kFileCreateOnly, 384) + eq(m.UV_EEXIST, err) + end) + + it('fails to open an symlink with kFileNoSymlink', function() + local err, _ = file_open(linkf, m.kFileNoSymlink, 384) + -- err is UV_EMLINK in FreeBSD, but if I use `ok(err == m.UV_ELOOP or err == + -- m.UV_EMLINK)`, then I loose the ability to see actual `err` value. + if err ~= m.UV_ELOOP then eq(m.UV_EMLINK, err) end + end) + + it('can open an existing file write-only with kFileCreate', function() + local err, fp = file_open(file1, m.kFileCreate, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can open an existing file read-only with zero', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can open an existing file read-only with kFileReadOnly', function() + local err, fp = file_open(file1, m.kFileReadOnly, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can open an existing file read-only with kFileNoSymlink', function() + local err, fp = file_open(file1, m.kFileNoSymlink, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_close(fp)) + end) + + it('can truncate an existing file with kFileTruncate', function() + local err, fp = file_open(file1, m.kFileTruncate, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + local attrs = lfs.attributes(file1) + eq(0, attrs.size) + end) + + it('can open an existing file write-only with kFileWriteOnly', function() + local err, fp = file_open(file1, m.kFileWriteOnly, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + local attrs = lfs.attributes(file1) + eq(4096, attrs.size) + end) + + it('fails to create a file with just kFileWriteOnly', function() + local err, _ = file_open(filec, m.kFileWriteOnly, 384) + eq(m.UV_ENOENT, err) + local attrs = lfs.attributes(filec) + eq(nil, attrs) + end) + + it('can truncate an existing file with kFileTruncate when opening a symlink', + function() + local err, fp = file_open(linkf, m.kFileTruncate, 384) + eq(0, err) + eq(true, fp.wr) + eq(0, m.file_close(fp)) + local attrs = lfs.attributes(file1) + eq(0, attrs.size) + end) + + it('fails to open a directory write-only', function() + local err, _ = file_open(dir, m.kFileWriteOnly, 384) + eq(m.UV_EISDIR, err) + end) + + it('fails to open a broken symbolic link write-only', function() + local err, _ = file_open(linkb, m.kFileWriteOnly, 384) + eq(m.UV_ENOENT, err) + end) + + it('fails to open a broken symbolic link read-only', function() + local err, _ = file_open(linkb, m.kFileReadOnly, 384) + eq(m.UV_ENOENT, err) + end) +end) + +describe('file_open_new', function() + it('can open a file read-only', function() + local err, fp = file_open_new(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq(0, m.file_free(fp)) + end) + + it('fails to open an existing file with kFileCreateOnly', function() + local err, fp = file_open_new(file1, m.kFileCreateOnly, 384) + eq(m.UV_EEXIST, err) + eq(nil, fp) + end) +end) + +-- file_close is called above, so it is not tested directly + +describe('file_fsync', function() + it('can flush writes to disk', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, file_fsync(fp)) + eq(0, err) + eq(0, lfs.attributes(filec).size) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, file_fsync(fp)) + eq(wsize, lfs.attributes(filec).size) + eq(0, m.file_close(fp)) + end) +end) + +describe('file_read', function() + it('can read small chunks of input until eof', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 3 + local exp_err = size + local exp_s = fcontents:sub(shift + 1, shift + size) + if shift + size >= #fcontents then + exp_err = #fcontents - shift + exp_s = (fcontents:sub(shift + 1, shift + size) + .. (('\0'):rep(size - exp_err))) + end + eq({exp_err, exp_s}, {file_read(fp, size)}) + shift = shift + size + end + eq(0, m.file_close(fp)) + end) + + it('can read the whole file at once', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) + eq({0, ('\0'):rep(#fcontents)}, {file_read(fp, #fcontents)}) + eq(0, m.file_close(fp)) + end) + + it('can read more then 1024 bytes after reading a small chunk', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq({5, fcontents:sub(1, 5)}, {file_read(fp, 5)}) + eq({#fcontents - 5, fcontents:sub(6) .. (('\0'):rep(5))}, + {file_read(fp, #fcontents)}) + eq(0, m.file_close(fp)) + end) + + it('can read file by 768-byte-chunks', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 768 + local exp_err = size + local exp_s = fcontents:sub(shift + 1, shift + size) + if shift + size >= #fcontents then + exp_err = #fcontents - shift + exp_s = (fcontents:sub(shift + 1, shift + size) + .. (('\0'):rep(size - exp_err))) + end + eq({exp_err, exp_s}, {file_read(fp, size)}) + shift = shift + size + end + eq(0, m.file_close(fp)) + end) +end) + +describe('file_write', function() + it('can write the whole file at once', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + eq(true, fp.wr) + local wr = file_write(fp, fcontents) + eq(#fcontents, wr) + eq(0, m.file_close(fp)) + eq(wr, lfs.attributes(filec).size) + eq(fcontents, io.open(filec):read('*a')) + end) + + it('can write the whole file by small chunks', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + eq(true, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 3 + local s = fcontents:sub(shift + 1, shift + size) + local wr = file_write(fp, s) + eq(wr, #s) + shift = shift + size + end + eq(0, m.file_close(fp)) + eq(#fcontents, lfs.attributes(filec).size) + eq(fcontents, io.open(filec):read('*a')) + end) + + it('can write the whole file by 768-byte-chunks', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + eq(true, fp.wr) + local shift = 0 + while shift < #fcontents do + local size = 768 + local s = fcontents:sub(shift + 1, shift + size) + local wr = file_write(fp, s) + eq(wr, #s) + shift = shift + size + end + eq(0, m.file_close(fp)) + eq(#fcontents, lfs.attributes(filec).size) + eq(fcontents, io.open(filec):read('*a')) + end) +end) + +describe('file_skip', function() + it('can skip 3 bytes', function() + local err, fp = file_open(file1, 0, 384) + eq(0, err) + eq(false, fp.wr) + eq(3, file_skip(fp, 3)) + local rd, s = file_read(fp, 3) + eq(3, rd) + eq(fcontents:sub(4, 6), s) + eq(0, m.file_close(fp)) + end) +end) diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index 857a5001f1..cc10b0cfa4 100644 --- a/test/unit/os/fs_spec.lua +++ b/test/unit/os/fs_spec.lua @@ -6,6 +6,7 @@ local helpers = require('test.unit.helpers') local cimport = helpers.cimport local cppimport = helpers.cppimport local internalize = helpers.internalize +local ok = helpers.ok local eq = helpers.eq local neq = helpers.neq local ffi = helpers.ffi @@ -27,6 +28,12 @@ cppimport('sys/stat.h') cppimport('fcntl.h') cppimport('uv-errno.h') +local s = '' +for i = 0, 255 do + s = s .. (i == 0 and '\0' or ('%c'):format(i)) +end +local fcontents = s:rep(16) + local buffer = "" local directory = nil local absolute_executable = nil @@ -150,11 +157,11 @@ describe('fs function', function() local function os_can_exe(name) local buf = ffi.new('char *[1]') buf[0] = NULL - local ok = fs.os_can_exe(to_cstr(name), buf, true) + local ce_ret = fs.os_can_exe(to_cstr(name), buf, true) -- When os_can_exe returns true, it must set the path. -- When it returns false, the path must be NULL. - if ok then + if ce_ret then neq(NULL, buf[0]) return internalize(buf[0]) else @@ -368,6 +375,51 @@ describe('fs function', function() local function os_open(path, flags, mode) return fs.os_open((to_cstr(path)), flags, mode) end + local function os_close(fd) + return fs.os_close(fd) + end + -- For some reason if length of NUL-bytes-string is the same as `char[?]` + -- size luajit crashes. Though it does not do so in this test suite, better + -- be cautios and allocate more elements then needed. I only did this to + -- strings. + local function os_read(fd, size) + local buf = nil + if size == nil then + size = 0 + else + buf = ffi.new('char[?]', size + 1, ('\0'):rep(size)) + end + local eof = ffi.new('bool[?]', 1, {true}) + local ret2 = fs.os_read(fd, eof, buf, size) + local ret1 = eof[0] + local ret3 = '' + if buf ~= nil then + ret3 = ffi.string(buf, size) + end + return ret1, ret2, ret3 + end + local function os_readv(fd, sizes) + local bufs = {} + for i, size in ipairs(sizes) do + bufs[i] = { + iov_base=ffi.new('char[?]', size + 1, ('\0'):rep(size)), + iov_len=size, + } + end + local iov = ffi.new('struct iovec[?]', #sizes, bufs) + local eof = ffi.new('bool[?]', 1, {true}) + local ret2 = fs.os_readv(fd, eof, iov, #sizes) + local ret1 = eof[0] + local ret3 = {} + for i = 1,#sizes do + -- Warning: iov may not be used. + ret3[i] = ffi.string(bufs[i].iov_base, bufs[i].iov_len) + end + return ret1, ret2, ret3 + end + local function os_write(fd, data) + return fs.os_write(fd, data, data and #data or 0) + end describe('os_file_exists', function() it('returns false when given a non-existing file', function() @@ -432,30 +484,34 @@ describe('fs function', function() end) describe('os_open', function() + local new_file = 'test_new_file' + local existing_file = 'unit-test-directory/test_existing.file' + before_each(function() - io.open('unit-test-directory/test_existing.file', 'w').close() + (io.open(existing_file, 'w')):close() end) after_each(function() - os.remove('unit-test-directory/test_existing.file') - os.remove('test_new_file') + os.remove(existing_file) + os.remove(new_file) end) - local new_file = 'test_new_file' - local existing_file = 'unit-test-directory/test_existing.file' - it('returns UV_ENOENT for O_RDWR on a non-existing file', function() eq(ffi.C.UV_ENOENT, (os_open('non-existing-file', ffi.C.kO_RDWR, 0))) end) - it('returns non-negative for O_CREAT on a non-existing file', function() + it('returns non-negative for O_CREAT on a non-existing file which then can be closed', function() assert_file_does_not_exist(new_file) - assert.is_true(0 <= (os_open(new_file, ffi.C.kO_CREAT, 0))) + local fd = os_open(new_file, ffi.C.kO_CREAT, 0) + assert.is_true(0 <= fd) + eq(0, os_close(fd)) end) - it('returns non-negative for O_CREAT on a existing file', function() + it('returns non-negative for O_CREAT on a existing file which then can be closed', function() assert_file_exists(existing_file) - assert.is_true(0 <= (os_open(existing_file, ffi.C.kO_CREAT, 0))) + local fd = os_open(existing_file, ffi.C.kO_CREAT, 0) + assert.is_true(0 <= fd) + eq(0, os_close(fd)) end) it('returns UV_EEXIST for O_CREAT|O_EXCL on a existing file', function() @@ -463,24 +519,181 @@ describe('fs function', function() eq(ffi.C.kUV_EEXIST, (os_open(existing_file, (bit.bor(ffi.C.kO_CREAT, ffi.C.kO_EXCL)), 0))) end) - it('sets `rwx` permissions for O_CREAT 700', function() + it('sets `rwx` permissions for O_CREAT 700 which then can be closed', function() assert_file_does_not_exist(new_file) --create the file - os_open(new_file, ffi.C.kO_CREAT, tonumber("700", 8)) + local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("700", 8)) --verify permissions eq('rwx------', lfs.attributes(new_file)['permissions']) + eq(0, os_close(fd)) end) - it('sets `rw` permissions for O_CREAT 600', function() + it('sets `rw` permissions for O_CREAT 600 which then can be closed', function() assert_file_does_not_exist(new_file) --create the file - os_open(new_file, ffi.C.kO_CREAT, tonumber("600", 8)) + local fd = os_open(new_file, ffi.C.kO_CREAT, tonumber("600", 8)) --verify permissions eq('rw-------', lfs.attributes(new_file)['permissions']) + eq(0, os_close(fd)) + end) + + it('returns a non-negative file descriptor for an existing file which then can be closed', function() + local fd = os_open(existing_file, ffi.C.kO_RDWR, 0) + assert.is_true(0 <= fd) + eq(0, os_close(fd)) + end) + end) + + describe('os_close', function() + it('returns EBADF for negative file descriptors', function() + eq(ffi.C.UV_EBADF, os_close(-1)) + eq(ffi.C.UV_EBADF, os_close(-1000)) + end) + end) + + describe('os_read', function() + local file = 'test-unit-os-fs_spec-os_read.dat' + + before_each(function() + local f = io.open(file, 'w') + f:write(fcontents) + f:close() + end) + + after_each(function() + os.remove(file) + end) + + it('can read zero bytes from a file', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 0, ''}, {os_read(fd, nil)}) + eq({false, 0, ''}, {os_read(fd, 0)}) + eq(0, os_close(fd)) + end) + + it('can read from a file multiple times', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 2, '\000\001'}, {os_read(fd, 2)}) + eq({false, 2, '\002\003'}, {os_read(fd, 2)}) + eq(0, os_close(fd)) + end) + + it('can read the whole file at once and then report eof', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, #fcontents, fcontents}, {os_read(fd, #fcontents)}) + eq({true, 0, ('\0'):rep(#fcontents)}, {os_read(fd, #fcontents)}) + eq(0, os_close(fd)) + end) + + it('can read the whole file in two calls, one partially', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, #fcontents * 3/4, fcontents:sub(1, #fcontents * 3/4)}, + {os_read(fd, #fcontents * 3/4)}) + eq({true, + (#fcontents * 1/4), + fcontents:sub(#fcontents * 3/4 + 1) .. ('\0'):rep(#fcontents * 2/4)}, + {os_read(fd, #fcontents * 3/4)}) + eq(0, os_close(fd)) + end) + end) + + describe('os_readv', function() + -- Function may be absent + if not pcall(function() return fs.os_readv end) then + return + end + local file = 'test-unit-os-fs_spec-os_readv.dat' + + before_each(function() + local f = io.open(file, 'w') + f:write(fcontents) + f:close() + end) + + after_each(function() + os.remove(file) + end) + + it('can read zero bytes from a file', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 0, {}}, {os_readv(fd, {})}) + eq({false, 0, {'', '', ''}}, {os_readv(fd, {0, 0, 0})}) + eq(0, os_close(fd)) + end) + + it('can read from a file multiple times to a differently-sized buffers', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, 2, {'\000\001'}}, {os_readv(fd, {2})}) + eq({false, 5, {'\002\003', '\004\005\006'}}, {os_readv(fd, {2, 3})}) + eq(0, os_close(fd)) + end) + + it('can read the whole file at once and then report eof', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, + #fcontents, + {fcontents:sub(1, #fcontents * 1/4), + fcontents:sub(#fcontents * 1/4 + 1, #fcontents * 3/4), + fcontents:sub(#fcontents * 3/4 + 1, #fcontents * 15/16), + fcontents:sub(#fcontents * 15/16 + 1, #fcontents)}}, + {os_readv(fd, {#fcontents * 1/4, + #fcontents * 2/4, + #fcontents * 3/16, + #fcontents * 1/16})}) + eq({true, 0, {'\0'}}, {os_readv(fd, {1})}) + eq(0, os_close(fd)) + end) + + it('can read the whole file in two calls, one partially', function() + local fd = os_open(file, ffi.C.kO_RDONLY, 0) + ok(fd >= 0) + eq({false, #fcontents * 3/4, {fcontents:sub(1, #fcontents * 3/4)}}, + {os_readv(fd, {#fcontents * 3/4})}) + eq({true, + (#fcontents * 1/4), + {fcontents:sub(#fcontents * 3/4 + 1) .. ('\0'):rep(#fcontents * 2/4)}}, + {os_readv(fd, {#fcontents * 3/4})}) + eq(0, os_close(fd)) + end) + end) + + describe('os_write', function() + -- Function may be absent + local file = 'test-unit-os-fs_spec-os_write.dat' + + before_each(function() + local f = io.open(file, 'w') + f:write(fcontents) + f:close() + end) + + after_each(function() + os.remove(file) + end) + + it('can write zero bytes to a file', function() + local fd = os_open(file, ffi.C.kO_WRONLY, 0) + ok(fd >= 0) + eq(0, os_write(fd, '')) + eq(0, os_write(fd, nil)) + eq(fcontents, io.open(file, 'r'):read('*a')) + eq(0, os_close(fd)) end) - it('returns a non-negative file descriptor for an existing file', function() - assert.is_true(0 <= (os_open(existing_file, ffi.C.kO_RDWR, 0))) + it('can write some data to a file', function() + local fd = os_open(file, ffi.C.kO_WRONLY, 0) + ok(fd >= 0) + eq(3, os_write(fd, 'abc')) + eq(4, os_write(fd, ' def')) + eq('abc def' .. fcontents:sub(8), io.open(file, 'r'):read('*a')) + eq(0, os_close(fd)) end) end) |