diff options
Diffstat (limited to 'src/nvim/shada.c')
-rw-r--r-- | src/nvim/shada.c | 4040 |
1 files changed, 4040 insertions, 0 deletions
diff --git a/src/nvim/shada.c b/src/nvim/shada.c new file mode 100644 index 0000000000..523f8db6f0 --- /dev/null +++ b/src/nvim/shada.c @@ -0,0 +1,4040 @@ +#ifdef HAVE_BE64TOH +# define _BSD_SOURCE 1 +# define _DEFAULT_SOURCE 1 +# include <endian.h> +#endif +#include <stdlib.h> +#include <stddef.h> +#include <stdbool.h> +#include <string.h> +#include <stdint.h> +#include <inttypes.h> +#include <errno.h> +#include <unistd.h> +#include <assert.h> + +#include <msgpack.h> +#include <uv.h> + +#include "nvim/os/os.h" +#include "nvim/os/time.h" +#include "nvim/vim.h" +#include "nvim/ascii.h" +#include "nvim/shada.h" +#include "nvim/message.h" +#include "nvim/globals.h" +#include "nvim/memory.h" +#include "nvim/mark.h" +#include "nvim/ops.h" +#include "nvim/garray.h" +#include "nvim/option.h" +#include "nvim/msgpack_rpc/helpers.h" +#include "nvim/api/private/defs.h" +#include "nvim/api/private/helpers.h" +#include "nvim/buffer.h" +#include "nvim/buffer_defs.h" +#include "nvim/misc2.h" +#include "nvim/ex_getln.h" +#include "nvim/search.h" +#include "nvim/eval.h" +#include "nvim/regexp.h" +#include "nvim/eval_defs.h" +#include "nvim/version.h" +#include "nvim/path.h" +#include "nvim/fileio.h" +#include "nvim/strings.h" +#include "nvim/lib/khash.h" +#include "nvim/lib/kvec.h" + +// Note: when using bufset hash pointers are intentionally casted to uintptr_t +// and not to khint32_t or khint64_t: this way compiler must give a warning +// (-Wconversion) when types change. +#ifdef ARCH_32 +KHASH_SET_INIT_INT(bufset) +#elif defined(ARCH_64) +KHASH_SET_INIT_INT64(bufset) +#else +# error Not a 64- or 32-bit architecture +#endif +KHASH_MAP_INIT_STR(fnamebufs, buf_T *) +KHASH_SET_INIT_STR(strset) + +#define copy_option_part(src, dest, ...) \ + ((char *) copy_option_part((char_u **) src, (char_u *) dest, __VA_ARGS__)) +#define find_shada_parameter(...) \ + ((const char *) find_shada_parameter(__VA_ARGS__)) +#define emsg2(a, b) emsg2((char_u *) a, (char_u *) b) +#define emsg3(a, b, c) emsg3((char_u *) a, (char_u *) b, (char_u *) c) +#define emsgu(a, ...) emsgu((char_u *) a, __VA_ARGS__) +#define home_replace_save(a, b) \ + ((char *)home_replace_save(a, (char_u *)b)) +#define home_replace(a, b, c, d, e) \ + home_replace(a, (char_u *)b, (char_u *)c, d, e) +#define vim_rename(a, b) \ + (vim_rename((char_u *)a, (char_u *)b)) +#define mb_strnicmp(a, b, c) \ + (mb_strnicmp((char_u *)a, (char_u *)b, c)) +#define has_non_ascii(a) (has_non_ascii((char_u *)a)) +#define string_convert(a, b, c) \ + ((char *)string_convert((vimconv_T *)a, (char_u *)b, c)) +#define path_shorten_fname_if_possible(b) \ + ((char *)path_shorten_fname_if_possible((char_u *)b)) +#define buflist_new(ffname, sfname, ...) \ + (buflist_new((char_u *)ffname, (char_u *)sfname, __VA_ARGS__)) +#define convert_setup(vcp, from, to) \ + (convert_setup(vcp, (char_u *)from, (char_u *)to)) +#define os_getperm(f) \ + (os_getperm((char_u *) f)) +#define os_isdir(f) (os_isdir((char_u *) f)) +#define regtilde(s, m) ((char *) regtilde((char_u *) s, m)) +#define path_tail_with_sep(f) ((char *) path_tail_with_sep((char_u *)f)) + +#define SEARCH_KEY_MAGIC "sm" +#define SEARCH_KEY_SMARTCASE "sc" +#define SEARCH_KEY_HAS_LINE_OFFSET "sl" +#define SEARCH_KEY_PLACE_CURSOR_AT_END "se" +#define SEARCH_KEY_IS_LAST_USED "su" +#define SEARCH_KEY_IS_SUBSTITUTE_PATTERN "ss" +#define SEARCH_KEY_HIGHLIGHTED "sh" +#define SEARCH_KEY_OFFSET "so" +#define SEARCH_KEY_PAT "sp" + +#define REG_KEY_TYPE "rt" +#define REG_KEY_WIDTH "rw" +#define REG_KEY_CONTENTS "rc" + +#define KEY_LNUM "l" +#define KEY_COL "c" +#define KEY_FILE "f" +#define KEY_NAME_CHAR "n" + +// Error messages formerly used by viminfo code: +// E136: viminfo: Too many errors, skipping rest of file +// E137: Viminfo file is not writable: %s +// E138: Can't write viminfo file %s! +// E195: Cannot open ShaDa file for reading +// E574: Unknown register type %d +// E575: Illegal starting char +// E576: Missing '>' +// E577: Illegal register name +// E886: Can't rename viminfo file to %s! +// Now only five of them are used: +// E137: ShaDa file is not writeable (for pre-open checks) +// E138: All %s.tmp.X files exist, cannot write ShaDa file! +// RCERR (E576) for critical read errors. +// RNERR (E136) for various errors when renaming. +// RERR (E575) for various errors inside read ShaDa file. +// SERR (E886) for various “system” errors (always contains output of +// strerror) + +/// Common prefix for all errors inside ShaDa file +/// +/// I.e. errors occurred while parsing, but not system errors occurred while +/// reading. +#define RERR "E575: " + +/// Common prefix for critical read errors +/// +/// I.e. errors that make shada_read_next_item return kSDReadStatusNotShaDa. +#define RCERR "E576: " + +/// Common prefix for all “system” errors +#define SERR "E886: " + +/// Common prefix for all “rename” errors +#define RNERR "E136: " + +/// Flags for shada_read_file and children +typedef enum { + kShaDaWantInfo = 1, ///< Load non-mark information + kShaDaWantMarks = 2, ///< Load local file marks and change list + kShaDaForceit = 4, ///< Overwrite info already read + kShaDaGetOldfiles = 8, ///< Load v:oldfiles. + kShaDaMissingError = 16, ///< Error out when os_open returns -ENOENT. +} ShaDaReadFileFlags; + +/// Possible ShaDa entry types +/// +/// @warning Enum values are part of the API and must not be altered. +/// +/// All values that are not in enum are ignored. +typedef enum { + kSDItemUnknown = -1, ///< Unknown item. + kSDItemMissing = 0, ///< Missing value. Should never appear in a file. + kSDItemHeader = 1, ///< Header. Present for debugging purposes. + kSDItemSearchPattern = 2, ///< Last search pattern (*not* history item). + ///< Comes from user searches (e.g. when typing + ///< "/pat") or :substitute command calls. + kSDItemSubString = 3, ///< Last substitute replacement string. + kSDItemHistoryEntry = 4, ///< History item. + kSDItemRegister = 5, ///< Register. + kSDItemVariable = 6, ///< Global variable. + kSDItemGlobalMark = 7, ///< Global mark definition. + kSDItemJump = 8, ///< Item from jump list. + kSDItemBufferList = 9, ///< Buffer list. + kSDItemLocalMark = 10, ///< Buffer-local mark. + kSDItemChange = 11, ///< Item from buffer change list. +#define SHADA_LAST_ENTRY ((uint64_t) kSDItemChange) +} ShadaEntryType; + +/// Possible results when reading ShaDa file +typedef enum { + kSDReadStatusSuccess, ///< Reading was successfull. + kSDReadStatusFinished, ///< Nothing more to read. + kSDReadStatusReadError, ///< Failed to read from file. + kSDReadStatusNotShaDa, ///< Input is most likely not a ShaDa file. + kSDReadStatusMalformed, ///< Error in the currently read item. +} ShaDaReadResult; + +/// Possible results of shada_write function. +typedef enum { + kSDWriteSuccessfull, ///< Writing was successfull. + kSDWriteReadNotShada, ///< Writing was successfull, but when reading it + ///< attempted to read file that did not look like + ///< a ShaDa file. + kSDWriteFailed, ///< Writing was not successfull (e.g. because there + ///< was no space left on device). +} ShaDaWriteResult; + +/// Flags for shada_read_next_item +enum SRNIFlags { + kSDReadHeader = (1 << kSDItemHeader), ///< Determines whether header should + ///< be read (it is usually ignored). + kSDReadUndisableableData = ( + (1 << kSDItemSearchPattern) + | (1 << kSDItemSubString) + | (1 << kSDItemJump)), ///< Data reading which cannot be disabled by &shada + ///< or other options except for disabling reading + ///< ShaDa as a whole. + kSDReadRegisters = (1 << kSDItemRegister), ///< Determines whether registers + ///< should be read (may only be + ///< disabled when writing, but + ///< not when reading). + kSDReadHistory = (1 << kSDItemHistoryEntry), ///< Determines whether history + ///< should be read (can only be + ///< disabled by &history). + kSDReadVariables = (1 << kSDItemVariable), ///< Determines whether variables + ///< should be read (disabled by + ///< removing ! from &shada). + kSDReadBufferList = (1 << kSDItemBufferList), ///< Determines whether buffer + ///< list should be read + ///< (disabled by removing + ///< % entry from &shada). + kSDReadUnknown = (1 << (SHADA_LAST_ENTRY + 1)), ///< Determines whether + ///< unknown items should be + ///< read (usually disabled). + kSDReadGlobalMarks = (1 << kSDItemGlobalMark), ///< Determines whether global + ///< marks should be read. Can + ///< only be disabled by + ///< having f0 in &shada when + ///< writing. + kSDReadLocalMarks = (1 << kSDItemLocalMark), ///< Determines whether local + ///< marks should be read. Can + ///< only be disabled by + ///< disabling &shada or putting + ///< '0 there. Is also used for + ///< v:oldfiles. + kSDReadChanges = (1 << kSDItemChange), ///< Determines whether change list + ///< should be read. Can only be + ///< disabled by disabling &shada or + ///< putting '0 there. +}; +// Note: SRNIFlags enum name was created only to make it possible to reference +// it. This name is not actually used anywhere outside of the documentation. + +/// Structure defining a single ShaDa file entry +typedef struct { + ShadaEntryType type; + Timestamp timestamp; + union { + Dictionary header; + struct shada_filemark { + char name; + pos_T mark; + char *fname; + dict_T *additional_data; + } filemark; + struct search_pattern { + bool magic; + bool smartcase; + bool has_line_offset; + bool place_cursor_at_end; + int64_t offset; + bool is_last_used; + bool is_substitute_pattern; + bool highlighted; + char *pat; + dict_T *additional_data; + } search_pattern; + struct history_item { + uint8_t histtype; + char *string; + char sep; + list_T *additional_elements; + } history_item; + struct reg { + char name; + uint8_t type; + char **contents; + size_t contents_size; + size_t width; + dict_T *additional_data; + } reg; + struct global_var { + char *name; + typval_T value; + list_T *additional_elements; + } global_var; + struct { + uint64_t type; + char *contents; + size_t size; + } unknown_item; + struct sub_string { + char *sub; + list_T *additional_elements; + } sub_string; + struct buffer_list { + size_t size; + struct buffer_list_buffer { + pos_T pos; + char *fname; + dict_T *additional_data; + } *buffers; + } buffer_list; + } data; +} ShadaEntry; + +struct hm_llist_entry; + +/// One entry in sized linked list +typedef struct hm_llist_entry { + ShadaEntry data; ///< Entry data. + bool can_free_entry; ///< True if data can be freed. + struct hm_llist_entry *next; ///< Pointer to next entry or NULL. + struct hm_llist_entry *prev; ///< Pointer to previous entry or NULL. +} HMLListEntry; + +KHASH_MAP_INIT_STR(hmll_entries, HMLListEntry *) + +/// Sized linked list structure for history merger +typedef struct { + HMLListEntry *entries; ///< Pointer to the start of the allocated array of + ///< entries. + HMLListEntry *first; ///< First entry in the list (is not necessary start + ///< of the array) or NULL. + HMLListEntry *last; ///< Last entry in the list or NULL. + HMLListEntry *free_entry; ///< Last free entry removed by hmll_remove. + HMLListEntry *last_free_entry; ///< Last unused element in entries array. + size_t size; ///< Number of allocated entries. + size_t num_entries; ///< Number of entries already used. + khash_t(hmll_entries) contained_entries; ///< Hash mapping all history entry + ///< strings to corresponding entry + ///< pointers. +} HMLList; + +typedef struct { + HMLList hmll; + bool do_merge; + bool reading; + const void *iter; + ShadaEntry last_hist_entry; + uint8_t history_type; +} HistoryMergerState; + +/// ShadaEntry structure that knows whether it should be freed +typedef struct { + ShadaEntry data; ///< ShadaEntry data. + bool can_free_entry; ///< True if entry can be freed. +} PossiblyFreedShadaEntry; + +/// Structure that holds one file marks. +typedef struct { + PossiblyFreedShadaEntry marks[NLOCALMARKS]; ///< All file marks. + PossiblyFreedShadaEntry changes[JUMPLISTSIZE]; ///< All file changes. + size_t changes_size; ///< Number of changes occupied. + ShadaEntry *additional_marks; ///< All marks with unknown names. + size_t additional_marks_size; ///< Size of the additional_marks array. + Timestamp greatest_timestamp; ///< Greatest timestamp among marks. +} FileMarks; + +KHASH_MAP_INIT_STR(file_marks, FileMarks) + +/// State structure used by shada_write +/// +/// Before actually writing most of the data is read to this structure. +typedef struct { + HistoryMergerState hms[HIST_COUNT]; ///< Structures for history merging. + PossiblyFreedShadaEntry global_marks[NGLOBALMARKS]; ///< All global marks. + PossiblyFreedShadaEntry registers[NUM_SAVED_REGISTERS]; ///< All registers. + PossiblyFreedShadaEntry jumps[JUMPLISTSIZE]; ///< All dumped jumps. + size_t jumps_size; ///< Number of jumps occupied. + PossiblyFreedShadaEntry search_pattern; ///< Last search pattern. + PossiblyFreedShadaEntry sub_search_pattern; ///< Last s/ search pattern. + PossiblyFreedShadaEntry replacement; ///< Last s// replacement string. + khash_t(strset) dumped_variables; ///< Names of already dumped variables. + khash_t(file_marks) file_marks; ///< All file marks. +} WriteMergerState; + +struct sd_read_def; + +/// Function used to close files defined by ShaDaReadDef +typedef void (*ShaDaReadCloser)(struct sd_read_def *const sd_reader) + REAL_FATTR_NONNULL_ALL; + +/// Function used to read ShaDa files +typedef ptrdiff_t (*ShaDaFileReader)(struct sd_read_def *const sd_reader, + void *const dest, + const size_t size) + REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Function used to skip in ShaDa files +typedef int (*ShaDaFileSkipper)(struct sd_read_def *const sd_reader, + const size_t offset) + REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Structure containing necessary pointers for reading ShaDa files +typedef struct sd_read_def { + ShaDaFileReader read; ///< Reader function. + ShaDaReadCloser close; ///< Close function. + 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. + 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 + ///< items. +} ShaDaReadDef; + +struct sd_write_def; + +/// Function used to close files defined by ShaDaWriteDef +typedef void (*ShaDaWriteCloser)(struct sd_write_def *const sd_writer) + REAL_FATTR_NONNULL_ALL; + +/// Function used to write ShaDa files +typedef ptrdiff_t (*ShaDaFileWriter)(struct sd_write_def *const sd_writer, + const void *const src, + const size_t size) + REAL_FATTR_NONNULL_ALL REAL_FATTR_WARN_UNUSED_RESULT; + +/// Structure containing necessary pointers for writing ShaDa files +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. + vimconv_T sd_conv; ///< Structure used for converting encodings of some + ///< items. +} ShaDaWriteDef; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "shada.c.generated.h" +#endif + +#define DEF_SDE(name, attr, ...) \ + [kSDItem##name] = { \ + .timestamp = 0, \ + .type = kSDItem##name, \ + .data = { \ + .attr = { __VA_ARGS__ } \ + } \ + } +#define DEFAULT_POS {1, 0, 0} +static const pos_T default_pos = DEFAULT_POS; +static const ShadaEntry sd_default_values[] = { + [kSDItemMissing] = { .type = kSDItemMissing, .timestamp = 0 }, + DEF_SDE(Header, header, .size = 0), + DEF_SDE(SearchPattern, search_pattern, + .magic = true, + .smartcase = false, + .has_line_offset = false, + .place_cursor_at_end = false, + .offset = 0, + .is_last_used = true, + .is_substitute_pattern = false, + .highlighted = false, + .pat = NULL, + .additional_data = NULL), + DEF_SDE(SubString, sub_string, .sub = NULL, .additional_elements = NULL), + DEF_SDE(HistoryEntry, history_item, + .histtype = HIST_CMD, + .string = NULL, + .sep = NUL, + .additional_elements = NULL), + DEF_SDE(Register, reg, + .name = NUL, + .type = MCHAR, + .contents = NULL, + .contents_size = 0, + .width = 0, + .additional_data = NULL), + DEF_SDE(Variable, global_var, + .name = NULL, + .value = { + .v_type = VAR_UNKNOWN, + .vval = { .v_string = NULL } + }, + .additional_elements = NULL), + DEF_SDE(GlobalMark, filemark, + .name = '"', + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), + DEF_SDE(Jump, filemark, + .name = NUL, + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), + DEF_SDE(BufferList, buffer_list, + .size = 0, + .buffers = NULL), + DEF_SDE(LocalMark, filemark, + .name = '"', + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), + DEF_SDE(Change, filemark, + .name = NUL, + .mark = DEFAULT_POS, + .fname = NULL, + .additional_data = NULL), +}; +#undef DEFAULT_POS +#undef DEF_SDE + +/// Initialize new linked list +/// +/// @param[out] hmll List to initialize. +/// @param[in] size Maximum size of the list. +static inline void hmll_init(HMLList *const hmll, const size_t size) + FUNC_ATTR_NONNULL_ALL +{ + *hmll = (HMLList) { + .entries = xcalloc(size, sizeof(hmll->entries[0])), + .first = NULL, + .last = NULL, + .free_entry = NULL, + .size = size, + .num_entries = 0, + .contained_entries = KHASH_EMPTY_TABLE(hmll_entries), + }; + hmll->last_free_entry = hmll->entries; +} + +/// Iterate over HMLList in forward direction +/// +/// @param hmll Pointer to the list. +/// @param cur_entry Name of the variable to iterate over. +/// +/// @return `for` cycle header (use `HMLL_FORALL(hmll, cur_entry) {body}`). +#define HMLL_FORALL(hmll, cur_entry) \ + for (HMLListEntry *cur_entry = (hmll)->first; cur_entry != NULL; \ + cur_entry = cur_entry->next) + +/// Remove entry from the linked list +/// +/// @param hmll List to remove from. +/// @param hmll_entry Entry to remove. +static inline void hmll_remove(HMLList *const hmll, + HMLListEntry *const hmll_entry) + FUNC_ATTR_NONNULL_ALL +{ + if (hmll_entry == hmll->last_free_entry - 1) { + hmll->last_free_entry--; + } else { + assert(hmll->free_entry == NULL); + hmll->free_entry = hmll_entry; + } + const khiter_t k = kh_get(hmll_entries, &hmll->contained_entries, + hmll_entry->data.data.history_item.string); + assert(k != kh_end(&hmll->contained_entries)); + kh_del(hmll_entries, &hmll->contained_entries, k); + if (hmll_entry->next == NULL) { + hmll->last = hmll_entry->prev; + } else { + hmll_entry->next->prev = hmll_entry->prev; + } + if (hmll_entry->prev == NULL) { + hmll->first = hmll_entry->next; + } else { + hmll_entry->prev->next = hmll_entry->next; + } + hmll->num_entries--; + if (hmll_entry->can_free_entry) { + shada_free_shada_entry(&hmll_entry->data); + } +} + + +/// Insert entry to the linked list +/// +/// @param[out] hmll List to insert to. +/// @param[in] hmll_entry Entry to insert after or NULL if it is needed +/// to insert at the first entry. +/// @param[in] data Data to insert. +/// @param[in] can_free_entry True if data can be freed. +static inline void hmll_insert(HMLList *const hmll, + HMLListEntry *hmll_entry, + const ShadaEntry data, + const bool can_free_entry) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (hmll->num_entries == hmll->size) { + if (hmll_entry == hmll->first) { + hmll_entry = NULL; + } + hmll_remove(hmll, hmll->first); + } + HMLListEntry *target_entry; + if (hmll->free_entry == NULL) { + assert((size_t) (hmll->last_free_entry - hmll->entries) + == hmll->num_entries); + target_entry = hmll->last_free_entry++; + } else { + assert((size_t) (hmll->last_free_entry - hmll->entries) - 1 + == hmll->num_entries); + target_entry = hmll->free_entry; + hmll->free_entry = NULL; + } + target_entry->data = data; + target_entry->can_free_entry = can_free_entry; + int kh_ret; + const khiter_t k = kh_put(hmll_entries, &hmll->contained_entries, + data.data.history_item.string, &kh_ret); + if (kh_ret > 0) { + kh_val(&hmll->contained_entries, k) = target_entry; + } + hmll->num_entries++; + target_entry->prev = hmll_entry; + if (hmll_entry == NULL) { + target_entry->next = hmll->first; + hmll->first = target_entry; + } else { + target_entry->next = hmll_entry->next; + hmll_entry->next = target_entry; + } + if (target_entry->next == NULL) { + hmll->last = target_entry; + } else { + target_entry->next->prev = target_entry; + } +} + +/// Iterate over HMLList in backward direction +/// +/// @param hmll Pointer to the list. +/// @param cur_entry Name of the variable to iterate over, must be already +/// defined. +/// +/// @return `for` cycle header (use `HMLL_FORALL(hmll, cur_entry) {body}`). +#define HMLL_ITER_BACK(hmll, cur_entry) \ + for (cur_entry = (hmll)->last; cur_entry != NULL; \ + cur_entry = cur_entry->prev) + +/// Free linked list +/// +/// @param[in] hmll List to free. +static inline void hmll_dealloc(HMLList *const hmll) + FUNC_ATTR_NONNULL_ALL +{ + kh_dealloc(hmll_entries, &hmll->contained_entries); + xfree(hmll->entries); +} + +/// Wrapper for reading from file descriptors +/// +/// @return -1 or number of bytes read. +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; + } + } + return (ptrdiff_t) read_bytes; +} + +/// Read one character +static int read_char(ShaDaReadDef *const sd_reader) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + uint8_t ret; + ptrdiff_t read_bytes = sd_reader->read(sd_reader, &ret, 1); + if (read_bytes != 1) { + return EOF; + } + return (int) ret; +} + +/// Wrapper for writing to file descriptors +/// +/// @return -1 or number of bytes written. +static ptrdiff_t write_file(ShaDaWriteDef *const sd_writer, + const void *const dest, + 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; + } + } + return (ptrdiff_t) written_bytes; +} + +/// 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); +} + +/// 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 (fsync(fd) < 0) { + emsg2(_(SERR "System error while synchronizing ShaDa file: %s"), + strerror(errno)); + errno = 0; + } + close_file(fd); +} + +/// Wrapper for read that reads to IObuff and ignores bytes read +/// +/// Used for skipping. +/// +/// @param[in,out] sd_reader File read. +/// @param[in] offset Amount of bytes to skip. +/// +/// @return FAIL in case of failure, OK in case of success. May set +/// sd_reader->eof or sd_reader->error. +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; + } + read_bytes += (size_t) new_read_bytes; + } while (read_bytes < offset && !sd_reader->eof); + + return (read_bytes == offset ? OK : FAIL); +} + +/// Wrapper for read that can be used when lseek cannot be used +/// +/// E.g. when trying to read from a pipe. +/// +/// @param[in,out] sd_reader File read. +/// @param[in] offset Amount of bytes to skip. +/// +/// @return kSDReadStatusReadError, kSDReadStatusNotShaDa or +/// kSDReadStatusSuccess. +static ShaDaReadResult sd_reader_skip(ShaDaReadDef *const sd_reader, + const size_t offset) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + if (sd_reader->skip(sd_reader, offset) != OK) { + if (sd_reader->error != NULL) { + emsg2(_(SERR "System error while skipping in ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else if (sd_reader->eof) { + emsgu(_(RCERR "Error while reading ShaDa file: " + "last entry specified that it occupies %" PRIu64 " bytes, " + "but file ended earlier"), + (uint64_t) offset); + return kSDReadStatusNotShaDa; + } + assert(false); + } + return kSDReadStatusSuccess; +} + +/// Wrapper for opening file descriptors +/// +/// All arguments are passed to os_open(). +/// +/// @return file descriptor or -errno 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 == ENOENT) { + return fd; + } + if (-fd == ENOMEM && !did_try_to_free) { + try_to_free_memory(); + did_try_to_free = true; + goto open_file_start; + } + if (-fd != EEXIST) { + emsg3(_(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. +/// @param[out] sd_reader Location where reader structure will be saved. +/// +/// @return -errno in case of error, 0 otherwise. +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; + } + + *sd_reader = (ShaDaReadDef) { + .read = &read_file, + .close = &close_sd_reader, + .skip = &sd_reader_skip_read, + .error = NULL, + .eof = false, + .fpos = 0, + .cookie = (void *) fd, + }; + + convert_setup(&sd_reader->sd_conv, "utf-8", p_enc); + + return 0; +} + +/// Wrapper for closing file descriptors +static void close_file(int fd) +{ +close_file_start: + if (close(fd) == -1) { + if (errno == EINTR) { + errno = 0; + goto close_file_start; + } else { + emsg2(_(SERR "System error while closing ShaDa file: %s"), + strerror(errno)); + errno = 0; + } + } +} + +/// Check whether buffer is in the given set +/// +/// @param[in] set Set to check within. +/// @param[in] buf Buffer to find. +/// +/// @return true or false. +static inline bool in_bufset(const khash_t(bufset) *const set, const buf_T *buf) + FUNC_ATTR_PURE +{ + return kh_get(bufset, set, (uintptr_t) buf) != kh_end(set); +} + +/// Check whether string is in the given set +/// +/// @param[in] set Set to check within. +/// @param[in] buf Buffer to find. +/// +/// @return true or false. +static inline bool in_strset(const khash_t(strset) *const set, char *str) + FUNC_ATTR_PURE +{ + return kh_get(strset, set, str) != kh_end(set); +} + +/// Msgpack callback for writing to ShaDaWriteDef* +static int msgpack_sd_writer_write(void *data, const char *buf, size_t len) +{ + ShaDaWriteDef *const sd_writer = (ShaDaWriteDef *) data; + ptrdiff_t written_bytes = sd_writer->write(sd_writer, buf, len); + if (written_bytes == -1) { + emsg2(_(SERR "System error while writing ShaDa file: %s"), + sd_writer->error); + return -1; + } + return 0; +} + +/// Check whether writing to shada file was disabled with -i NONE +/// +/// @return true if it was disabled, false otherwise. +static bool shada_disabled(void) + FUNC_ATTR_PURE +{ + return used_shada_file != NULL && STRCMP(used_shada_file, "NONE") == 0; +} + +/// Read ShaDa file +/// +/// @param[in] file File to read or NULL to use default name. +/// @param[in] flags Flags, see ShaDaReadFileFlags enum. +/// +/// @return FAIL if reading failed for some reason and OK otherwise. +static int shada_read_file(const char *const file, const int flags) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (shada_disabled()) { + return FAIL; + } + + char *const fname = shada_filename(file); + + ShaDaReadDef sd_reader; + const int of_ret = open_shada_file_for_reading(fname, &sd_reader); + + if (p_verbose > 0) { + verbose_enter(); + smsg(_("Reading ShaDa file \"%s\"%s%s%s"), + fname, + (flags & kShaDaWantInfo) ? _(" info") : "", + (flags & kShaDaWantMarks) ? _(" marks") : "", + (flags & kShaDaGetOldfiles) ? _(" oldfiles") : "", + of_ret != 0 ? _(" FAILED") : ""); + verbose_leave(); + } + + if (of_ret != 0) { + if (-of_ret == ENOENT && (flags & kShaDaMissingError)) { + emsg3(_(SERR "System error while opening ShaDa file %s for reading: %s"), + fname, os_strerror(of_ret)); + } + xfree(fname); + return FAIL; + } + xfree(fname); + + shada_read(&sd_reader, flags); + sd_reader.close(&sd_reader); + + return OK; +} + +/// Wrapper for hist_iter() function which produces ShadaEntry values +/// +/// @param[in] iter Current iteration state. +/// @param[in] history_type Type of the history (HIST_*). +/// @param[in] zero If true, then item is removed from instance +/// memory upon reading. +/// @param[out] hist Location where iteration results should be saved. +/// +/// @return Next iteration state. +static const void *shada_hist_iter(const void *const iter, + const uint8_t history_type, + const bool zero, + ShadaEntry *const hist) + FUNC_ATTR_NONNULL_ARG(4) FUNC_ATTR_WARN_UNUSED_RESULT +{ + histentry_T hist_he; + const void *const ret = hist_iter(iter, history_type, zero, &hist_he); + if (hist_he.hisstr == NULL) { + *hist = (ShadaEntry) { .type = kSDItemMissing }; + } else { + *hist = (ShadaEntry) { + .type = kSDItemHistoryEntry, + .timestamp = hist_he.timestamp, + .data = { + .history_item = { + .histtype = history_type, + .string = (char *) hist_he.hisstr, + .sep = (char) (history_type == HIST_SEARCH + ? (char) hist_he.hisstr[STRLEN(hist_he.hisstr) + 1] + : 0), + .additional_elements = hist_he.additional_elements, + } + } + }; + } + return ret; +} + +/// Insert history entry +/// +/// Inserts history entry at the end of the ring buffer (may insert earlier +/// according to the timestamp). If entry was already in the ring buffer +/// existing entry will be removed unless it has greater timestamp. +/// +/// Before the new entry entries from the current Neovim history will be +/// inserted unless `do_iter` argument is false. +/// +/// @param[in,out] hms_p Ring buffer and associated structures. +/// @param[in] entry Inserted entry. +/// @param[in] do_iter Determines whether Neovim own history should +/// be used. Must be true only if inserting +/// entry from current Neovim history. +/// @param[in] can_free_entry True if entry can be freed. +static void hms_insert(HistoryMergerState *const hms_p, const ShadaEntry entry, + const bool do_iter, const bool can_free_entry) + FUNC_ATTR_NONNULL_ALL +{ + if (do_iter) { + while (hms_p->last_hist_entry.type != kSDItemMissing + && hms_p->last_hist_entry.timestamp < entry.timestamp) { + hms_insert(hms_p, hms_p->last_hist_entry, false, hms_p->reading); + if (hms_p->iter == NULL) { + hms_p->last_hist_entry.type = kSDItemMissing; + break; + } + hms_p->iter = shada_hist_iter(hms_p->iter, hms_p->history_type, + hms_p->reading, &hms_p->last_hist_entry); + } + } + HMLList *const hmll = &hms_p->hmll; + const khiter_t k = kh_get(hmll_entries, &hms_p->hmll.contained_entries, + entry.data.history_item.string); + if (k != kh_end(&hmll->contained_entries)) { + HMLListEntry *const existing_entry = kh_val(&hmll->contained_entries, k); + if (entry.timestamp > existing_entry->data.timestamp) { + hmll_remove(hmll, existing_entry); + } else if (!do_iter && entry.timestamp == existing_entry->data.timestamp) { + // Prefer entry from the current Neovim instance. + if (existing_entry->can_free_entry) { + shada_free_shada_entry(&existing_entry->data); + } + existing_entry->data = entry; + existing_entry->can_free_entry = can_free_entry; + // Previous key was freed above, as part of freeing the ShaDa entry. + kh_key(&hmll->contained_entries, k) = entry.data.history_item.string; + return; + } else { + return; + } + } + HMLListEntry *insert_after; + HMLL_ITER_BACK(hmll, insert_after) { + if (insert_after->data.timestamp <= entry.timestamp) { + break; + } + } + hmll_insert(hmll, insert_after, entry, can_free_entry); +} + +/// Initialize the history merger +/// +/// @param[out] hms_p Structure to be initialized. +/// @param[in] history_type History type (one of HIST_\* values). +/// @param[in] num_elements Number of elements in the result. +/// @param[in] do_merge Prepare structure for merging elements. +/// @param[in] reading If true, then merger is reading history for use +/// in Neovim. +static inline void hms_init(HistoryMergerState *const hms_p, + const uint8_t history_type, + const size_t num_elements, + const bool do_merge, + const bool reading) + FUNC_ATTR_NONNULL_ALL +{ + hmll_init(&hms_p->hmll, num_elements); + hms_p->do_merge = do_merge; + hms_p->reading = reading; + hms_p->iter = shada_hist_iter(NULL, history_type, hms_p->reading, + &hms_p->last_hist_entry); + hms_p->history_type = history_type; +} + +/// Merge in all remaining Neovim own history entries +/// +/// @param[in,out] hms_p Merger structure into which history should be +/// inserted. +static inline void hms_insert_whole_neovim_history( + HistoryMergerState *const hms_p) + FUNC_ATTR_NONNULL_ALL +{ + while (hms_p->last_hist_entry.type != kSDItemMissing) { + hms_insert(hms_p, hms_p->last_hist_entry, false, hms_p->reading); + if (hms_p->iter == NULL) { + break; + } + hms_p->iter = shada_hist_iter(hms_p->iter, hms_p->history_type, + hms_p->reading, &hms_p->last_hist_entry); + } +} + +/// Convert merger structure to Neovim internal structure for history +/// +/// @param[in] hms_p Converted merger structure. +/// @param[out] hist_array Array with the results. +/// @param[out] new_hisidx New last history entry index. +/// @param[out] new_hisnum Amount of history items in merger structure. +static inline void hms_to_he_array(const HistoryMergerState *const hms_p, + histentry_T *const hist_array, + int *const new_hisidx, + int *const new_hisnum) + FUNC_ATTR_NONNULL_ALL +{ + histentry_T *hist = hist_array; + HMLL_FORALL(&hms_p->hmll, cur_entry) { + hist->timestamp = cur_entry->data.timestamp; + hist->hisnum = (int) (hist - hist_array) + 1; + hist->hisstr = (char_u *) cur_entry->data.data.history_item.string; + hist->additional_elements = + cur_entry->data.data.history_item.additional_elements; + hist++; + } + *new_hisnum = (int) (hist - hist_array); + *new_hisidx = *new_hisnum - 1; +} + +/// Free history merger structure +/// +/// @param[in] hms_p Structure to be freed. +static inline void hms_dealloc(HistoryMergerState *const hms_p) + FUNC_ATTR_NONNULL_ALL +{ + hmll_dealloc(&hms_p->hmll); +} + +/// Iterate over all history entries in history merger, in order +/// +/// @param[in] hms_p Merger structure to iterate over. +/// @param[out] cur_entry Name of the iterator variable. +/// +/// @return for cycle header. Use `HMS_ITER(hms_p, cur_entry) {body}`. +#define HMS_ITER(hms_p, cur_entry) \ + HMLL_FORALL(&((hms_p)->hmll), cur_entry) + +/// Find buffer for given buffer name (cached) +/// +/// @param[in,out] fname_bufs Cache containing fname to buffer mapping. +/// @param[in] fname File name to find. +/// +/// @return Pointer to the buffer or NULL. +static buf_T *find_buffer(khash_t(fnamebufs) *const fname_bufs, + const char *const fname) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ALL +{ + int kh_ret; + khint_t k = kh_put(fnamebufs, fname_bufs, fname, &kh_ret); + if (!kh_ret) { + return kh_val(fname_bufs, k); + } + kh_key(fname_bufs, k) = xstrdup(fname); + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL) { + if (fnamecmp(fname, buf->b_ffname) == 0) { + kh_val(fname_bufs, k) = buf; + return buf; + } + } + } + kh_val(fname_bufs, k) = NULL; + return NULL; +} + +/// Compare two marks +static inline bool marks_equal(const pos_T a, const pos_T b) +{ + return (a.lnum == b.lnum) && (a.col == b.col); +} + +#define MERGE_JUMPS(jumps_size, jumps, jumps_type, timestamp_attr, mark_attr, \ + entry, fname_cond, free_func, fin_func, \ + idxadj_func, afterfree_func) \ + do { \ + const int jl_len = (int) jumps_size; \ + int i; \ + for (i = jl_len; i > 0; i--) { \ + const jumps_type jl_entry = jumps[i - 1]; \ + if (jl_entry.timestamp_attr <= entry.timestamp) { \ + if (marks_equal(jl_entry.mark_attr, entry.data.filemark.mark) \ + && fname_cond) { \ + i = -1; \ + } \ + break; \ + } \ + } \ + if (i > 0) { \ + if (jl_len == JUMPLISTSIZE) { \ + free_func(jumps[0]); \ + i--; \ + if (i > 0) { \ + memmove(&jumps[0], &jumps[1], sizeof(jumps[1]) * (size_t) i); \ + } \ + } else if (i != jl_len) { \ + memmove(&jumps[i + 1], &jumps[i], \ + sizeof(jumps[0]) * (size_t) (jl_len - i)); \ + } \ + } else if (i == 0) { \ + if (jl_len == JUMPLISTSIZE) { \ + i = -1; \ + } else if (jl_len > 0) { \ + memmove(&jumps[1], &jumps[0], sizeof(jumps[0]) * (size_t) jl_len); \ + } \ + } \ + if (i != -1) { \ + jumps[i] = fin_func(entry); \ + if (jl_len < JUMPLISTSIZE) { \ + jumps_size++; \ + } \ + idxadj_func(i); \ + } else { \ + shada_free_shada_entry(&entry); \ + afterfree_func(entry); \ + } \ + } while (0) + +/// Read data from ShaDa file +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[in] flags What to read, see ShaDaReadFileFlags enum. +static void shada_read(ShaDaReadDef *const sd_reader, const int flags) + FUNC_ATTR_NONNULL_ALL +{ + list_T *oldfiles_list = get_vim_var_list(VV_OLDFILES); + const bool force = flags & kShaDaForceit; + const bool get_old_files = (flags & (kShaDaGetOldfiles | kShaDaForceit) + && (force || oldfiles_list == NULL + || oldfiles_list->lv_len == 0)); + const bool want_marks = flags & kShaDaWantMarks; + const unsigned srni_flags = (unsigned) ( + (flags & kShaDaWantInfo + ? (kSDReadUndisableableData + | kSDReadRegisters + | kSDReadGlobalMarks + | (p_hi ? kSDReadHistory : 0) + | (find_shada_parameter('!') != NULL + ? kSDReadVariables + : 0) + | (find_shada_parameter('%') != NULL + && ARGCOUNT == 0 + ? kSDReadBufferList + : 0)) + : 0) + | (want_marks && get_shada_parameter('\'') > 0 + ? kSDReadLocalMarks | kSDReadChanges + : 0) + | (get_old_files + ? kSDReadLocalMarks + : 0)); + if (srni_flags == 0) { + // Nothing to do. + return; + } + HistoryMergerState hms[HIST_COUNT]; + if (srni_flags & kSDReadHistory) { + for (uint8_t i = 0; i < HIST_COUNT; i++) { + hms_init(&hms[i], i, (size_t) p_hi, true, true); + } + } + ShadaEntry cur_entry; + khash_t(bufset) cl_bufs = KHASH_EMPTY_TABLE(bufset); + khash_t(fnamebufs) fname_bufs = KHASH_EMPTY_TABLE(fnamebufs); + khash_t(strset) oldfiles_set = KHASH_EMPTY_TABLE(strset); + if (get_old_files && (oldfiles_list == NULL || force)) { + oldfiles_list = list_alloc(); + set_vim_var_list(VV_OLDFILES, oldfiles_list); + } + ShaDaReadResult srni_ret; + while ((srni_ret = shada_read_next_item(sd_reader, &cur_entry, srni_flags, 0)) + != kSDReadStatusFinished) { + switch (srni_ret) { + case kSDReadStatusSuccess: { + break; + } + case kSDReadStatusFinished: { + // Should be handled by the while condition. + assert(false); + } + case kSDReadStatusNotShaDa: + case kSDReadStatusReadError: { + goto shada_read_main_cycle_end; + } + case kSDReadStatusMalformed: { + continue; + } + } + switch (cur_entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + break; + } + case kSDItemHeader: { + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemSearchPattern: { + if (!force) { + SearchPattern pat; + (cur_entry.data.search_pattern.is_substitute_pattern + ? &get_substitute_pattern + : &get_search_pattern)(&pat); + if (pat.pat != NULL && pat.timestamp >= cur_entry.timestamp) { + shada_free_shada_entry(&cur_entry); + break; + } + } + (cur_entry.data.search_pattern.is_substitute_pattern + ? &set_substitute_pattern + : &set_search_pattern)((SearchPattern) { + .magic = cur_entry.data.search_pattern.magic, + .no_scs = !cur_entry.data.search_pattern.smartcase, + .off = { + .line = cur_entry.data.search_pattern.has_line_offset, + .end = cur_entry.data.search_pattern.place_cursor_at_end, + .off = cur_entry.data.search_pattern.offset, + }, + .pat = (char_u *) cur_entry.data.search_pattern.pat, + .additional_data = cur_entry.data.search_pattern.additional_data, + .timestamp = cur_entry.timestamp, + }); + if (cur_entry.data.search_pattern.is_last_used) { + set_last_used_pattern( + cur_entry.data.search_pattern.is_substitute_pattern); + } + if (cur_entry.data.search_pattern.is_last_used) { + SET_NO_HLSEARCH(!cur_entry.data.search_pattern.highlighted); + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemSubString: { + if (!force) { + SubReplacementString sub; + sub_get_replacement(&sub); + if (sub.sub != NULL && sub.timestamp >= cur_entry.timestamp) { + shada_free_shada_entry(&cur_entry); + break; + } + } + sub_set_replacement((SubReplacementString) { + .sub = cur_entry.data.sub_string.sub, + .timestamp = cur_entry.timestamp, + .additional_elements = cur_entry.data.sub_string.additional_elements, + }); + // Without using regtilde and without / &cpo flag previous substitute + // string is close to useless: you can only use it with :& or :~ and + // that’s all because s//~ is not available until the first call to + // regtilde. Vim was not calling this for some reason. + (void) regtilde(cur_entry.data.sub_string.sub, p_magic); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemHistoryEntry: { + if (cur_entry.data.history_item.histtype >= HIST_COUNT) { + shada_free_shada_entry(&cur_entry); + break; + } + hms_insert(hms + cur_entry.data.history_item.histtype, cur_entry, true, + true); + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemRegister: { + if (cur_entry.data.reg.type != MCHAR + && cur_entry.data.reg.type != MLINE + && cur_entry.data.reg.type != MBLOCK) { + shada_free_shada_entry(&cur_entry); + break; + } + if (!force) { + const yankreg_T *const reg = op_register_get(cur_entry.data.reg.name); + if (reg == NULL || reg->timestamp >= cur_entry.timestamp) { + shada_free_shada_entry(&cur_entry); + break; + } + } + if (!op_register_set(cur_entry.data.reg.name, (yankreg_T) { + .y_array = (char_u **) cur_entry.data.reg.contents, + .y_size = (linenr_T) cur_entry.data.reg.contents_size, + .y_type = cur_entry.data.reg.type, + .y_width = (colnr_T) cur_entry.data.reg.width, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.reg.additional_data, + })) { + shada_free_shada_entry(&cur_entry); + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemVariable: { + var_set_global(cur_entry.data.global_var.name, + cur_entry.data.global_var.value); + cur_entry.data.global_var.value.v_type = VAR_UNKNOWN; + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemJump: + case kSDItemGlobalMark: { + buf_T *buf = find_buffer(&fname_bufs, cur_entry.data.filemark.fname); + if (buf != NULL) { + xfree(cur_entry.data.filemark.fname); + cur_entry.data.filemark.fname = NULL; + } + xfmark_T fm = (xfmark_T) { + .fname = (char_u *) (buf == NULL + ? cur_entry.data.filemark.fname + : NULL), + .fmark = { + .mark = cur_entry.data.filemark.mark, + .fnum = (buf == NULL ? 0 : buf->b_fnum), + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }, + }; + if (cur_entry.type == kSDItemGlobalMark) { + if (!mark_set_global(cur_entry.data.filemark.name, fm, !force)) { + shada_free_shada_entry(&cur_entry); + break; + } + } else { +#define SDE_TO_XFMARK(entry) fm +#define ADJUST_IDX(i) \ + if (curwin->w_jumplistidx >= i \ + && curwin->w_jumplistidx + 1 <= curwin->w_jumplistlen) { \ + curwin->w_jumplistidx++; \ + } +#define DUMMY_AFTERFREE(entry) + MERGE_JUMPS(curwin->w_jumplistlen, curwin->w_jumplist, xfmark_T, + fmark.timestamp, fmark.mark, cur_entry, + (buf == NULL + ? (jl_entry.fname != NULL + && STRCMP(fm.fname, jl_entry.fname) == 0) + : fm.fmark.fnum == jl_entry.fmark.fnum), + free_xfmark, SDE_TO_XFMARK, ADJUST_IDX, DUMMY_AFTERFREE); +#undef SDE_TO_XFMARK +#undef ADJUST_IDX +#undef DUMMY_AFTERFREE + } + // Do not free shada entry: its allocated memory was saved above. + break; + } + case kSDItemBufferList: { + for (size_t i = 0; i < cur_entry.data.buffer_list.size; i++) { + char *const sfname = path_shorten_fname_if_possible( + cur_entry.data.buffer_list.buffers[i].fname); + buf_T *const buf = buflist_new( + cur_entry.data.buffer_list.buffers[i].fname, sfname, 0, + BLN_LISTED); + if (buf != NULL) { + RESET_FMARK(&buf->b_last_cursor, + cur_entry.data.buffer_list.buffers[i].pos, 0); + buflist_setfpos(buf, curwin, buf->b_last_cursor.mark.lnum, + buf->b_last_cursor.mark.col, false); + buf->additional_data = + cur_entry.data.buffer_list.buffers[i].additional_data; + cur_entry.data.buffer_list.buffers[i].additional_data = NULL; + } + } + shada_free_shada_entry(&cur_entry); + break; + } + case kSDItemChange: + case kSDItemLocalMark: { + if (get_old_files && !in_strset(&oldfiles_set, + cur_entry.data.filemark.fname)) { + char *fname = cur_entry.data.filemark.fname; + if (want_marks) { + // Do not bother with allocating memory for the string if already + // allocated string from cur_entry can be used. It cannot be used if + // want_marks is set because this way it may be used for a mark. + fname = xstrdup(fname); + } + int kh_ret; + (void) kh_put(strset, &oldfiles_set, fname, &kh_ret); + list_append_allocated_string(oldfiles_list, fname); + if (!want_marks) { + // Avoid free because this string was already used. + cur_entry.data.filemark.fname = NULL; + } + } + if (!want_marks) { + shada_free_shada_entry(&cur_entry); + break; + } + buf_T *buf = find_buffer(&fname_bufs, cur_entry.data.filemark.fname); + if (buf == NULL) { + shada_free_shada_entry(&cur_entry); + break; + } + const fmark_T fm = (fmark_T) { + .mark = cur_entry.data.filemark.mark, + .fnum = 0, + .timestamp = cur_entry.timestamp, + .additional_data = cur_entry.data.filemark.additional_data, + }; + if (cur_entry.type == kSDItemLocalMark) { + if (!mark_set_local(cur_entry.data.filemark.name, buf, fm, !force)) { + shada_free_shada_entry(&cur_entry); + break; + } + } else { + int kh_ret; + (void) kh_put(bufset, &cl_bufs, (uintptr_t) buf, &kh_ret); +#define SDE_TO_FMARK(entry) fm +#define AFTERFREE(entry) (entry).data.filemark.fname = NULL +#define DUMMY_IDX_ADJ(i) + MERGE_JUMPS(buf->b_changelistlen, buf->b_changelist, fmark_T, + timestamp, mark, cur_entry, true, + free_fmark, SDE_TO_FMARK, DUMMY_IDX_ADJ, AFTERFREE); +#undef SDE_TO_FMARK +#undef AFTERFREE +#undef DUMMY_IDX_ADJ + } + // Do not free shada entry: except for fname, its allocated memory (i.e. + // additional_data attribute contents if non-NULL) was saved above. + xfree(cur_entry.data.filemark.fname); + break; + } + } + } +shada_read_main_cycle_end: + // Warning: shada_hist_iter returns ShadaEntry elements which use strings from + // original history list. This means that once such entry is removed + // from the history Neovim array will no longer be valid. To reduce + // amount of memory allocations ShaDa file reader allocates enough + // memory for the history string itself and separator character which + // may be assigned right away. + if (srni_flags & kSDReadHistory) { + for (uint8_t i = 0; i < HIST_COUNT; i++) { + hms_insert_whole_neovim_history(&hms[i]); + clr_history(i); + int *new_hisidx; + int *new_hisnum; + histentry_T *hist = hist_get_array(i, &new_hisidx, &new_hisnum); + if (hist != NULL) { + hms_to_he_array(&hms[i], hist, new_hisidx, new_hisnum); + } + hms_dealloc(&hms[i]); + } + } + if (cl_bufs.n_occupied) { + FOR_ALL_TAB_WINDOWS(tp, wp) { + (void) tp; + if (in_bufset(&cl_bufs, wp->w_buffer)) { + wp->w_changelistidx = wp->w_buffer->b_changelistlen; + } + } + } + kh_dealloc(bufset, &cl_bufs); + const char *key; + kh_foreach_key(&fname_bufs, key, { + xfree((void *) key); + }) + kh_dealloc(fnamebufs, &fname_bufs); + kh_dealloc(strset, &oldfiles_set); +} + +/// Get the ShaDa file name to use +/// +/// If "file" is given and not empty, use it (has already been expanded by +/// cmdline functions). Otherwise use "-i file_name", value from 'shada' or the +/// default, and expand environment variables. +/// +/// @param[in] file Forced file name or NULL. +/// +/// @return An allocated string containing shada file name. +static char *shada_filename(const char *file) + FUNC_ATTR_MALLOC FUNC_ATTR_NONNULL_RET FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (file == NULL || *file == NUL) { + if (used_shada_file != NULL) { + file = used_shada_file; + } else { + if ((file = find_shada_parameter('n')) == NULL || *file == NUL) { +#ifdef SHADA_FILE2 + // don't use $HOME when not defined (turned into "c:/"!). + if (os_getenv((char_u *)"HOME") == NULL) { + // don't use $VIM when not available. + expand_env((char_u *)"$VIM", NameBuff, MAXPATHL); + if (STRCMP("$VIM", NameBuff) != 0) { // $VIM was expanded + file = SHADA_FILE2; + } else { + file = SHADA_FILE; + } + } else { +#endif + file = SHADA_FILE; +#ifdef SHADA_FILE2 + } +#endif + } + // XXX It used to be one level lower, so that whatever is in + // `used_shada_file` was expanded. I intentionally moved it here + // because various expansions must have already be done by the shell. + // If shell is not performing them then they should be done in main.c + // where arguments are parsed, *not here*. + expand_env((char_u *)file, &(NameBuff[0]), MAXPATHL); + file = (const char *) &(NameBuff[0]); + } + } + return xstrdup(file); +} + +#define PACK_STATIC_STR(s) \ + do { \ + msgpack_pack_str(spacker, sizeof(s) - 1); \ + msgpack_pack_str_body(spacker, s, sizeof(s) - 1); \ + } while (0) +#define PACK_STRING(s) \ + do { \ + const String s_ = (s); \ + msgpack_pack_str(spacker, s_.size); \ + msgpack_pack_str_body(spacker, s_.data, s_.size); \ + } while (0) +#define PACK_BIN(s) \ + do { \ + const String s_ = (s); \ + msgpack_pack_bin(spacker, s_.size); \ + msgpack_pack_bin_body(spacker, s_.data, s_.size); \ + } while (0) + +/// Write single ShaDa entry +/// +/// @param[in] packer Packer used to write entry. +/// @param[in] entry Entry written. +/// @param[in] max_kbyte Maximum size of an item in KiB. Zero means no +/// restrictions. +static bool shada_pack_entry(msgpack_packer *const packer, + ShadaEntry entry, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL +{ + msgpack_sbuffer sbuf; + msgpack_sbuffer_init(&sbuf); + msgpack_packer *spacker = msgpack_packer_new(&sbuf, &msgpack_sbuffer_write); +#define DUMP_ADDITIONAL_ELEMENTS(src) \ + do { \ + if ((src) != NULL) { \ + for (listitem_T *li = (src)->lv_first; li != NULL; li = li->li_next) { \ + if (vim_to_msgpack(spacker, &li->li_tv) == FAIL) { \ + goto shada_pack_entry_error; \ + } \ + } \ + } \ + } while (0) +#define DUMP_ADDITIONAL_DATA(src) \ + do { \ + dict_T *const d = (src); \ + if (d != NULL) { \ + size_t todo = d->dv_hashtab.ht_used; \ + for (const hashitem_T *hi= d->dv_hashtab.ht_array; todo; hi++) { \ + if (!HASHITEM_EMPTY(hi)) { \ + todo--; \ + dictitem_T *const di = HI2DI(hi); \ + const size_t key_len = strlen((const char *) hi->hi_key); \ + msgpack_pack_str(spacker, key_len); \ + msgpack_pack_str_body(spacker, (const char *) hi->hi_key, key_len); \ + if (vim_to_msgpack(spacker, &di->di_tv) == FAIL) { \ + goto shada_pack_entry_error; \ + } \ + } \ + } \ + } \ + } while (0) +#define CHECK_DEFAULT(entry, attr) \ + (sd_default_values[entry.type].data.attr == entry.data.attr) +#define ONE_IF_NOT_DEFAULT(entry, attr) \ + ((size_t) (!CHECK_DEFAULT(entry, attr))) + switch (entry.type) { + case kSDItemMissing: { + assert(false); + } + case kSDItemUnknown: { + if (spacker->callback(spacker->data, entry.data.unknown_item.contents, + (unsigned) entry.data.unknown_item.size) == -1) { + goto shada_pack_entry_error; + } + break; + } + case kSDItemHistoryEntry: { + const bool is_hist_search = + entry.data.history_item.histtype == HIST_SEARCH; + const size_t arr_size = 2 + (size_t) is_hist_search + (size_t) ( + entry.data.history_item.additional_elements == NULL + ? 0 + : entry.data.history_item.additional_elements->lv_len); + msgpack_pack_array(spacker, arr_size); + msgpack_pack_uint8(spacker, entry.data.history_item.histtype); + PACK_BIN(cstr_as_string(entry.data.history_item.string)); + if (is_hist_search) { + msgpack_pack_uint8(spacker, (uint8_t) entry.data.history_item.sep); + } + DUMP_ADDITIONAL_ELEMENTS(entry.data.history_item.additional_elements); + break; + } + case kSDItemVariable: { + const size_t arr_size = 2 + (size_t) ( + entry.data.global_var.additional_elements == NULL + ? 0 + : entry.data.global_var.additional_elements->lv_len); + msgpack_pack_array(spacker, arr_size); + PACK_BIN(cstr_as_string(entry.data.global_var.name)); + if (vim_to_msgpack(spacker, &entry.data.global_var.value) == FAIL) { + goto shada_pack_entry_error; + } + DUMP_ADDITIONAL_ELEMENTS(entry.data.global_var.additional_elements); + break; + } + case kSDItemSubString: { + const size_t arr_size = 1 + (size_t) ( + entry.data.sub_string.additional_elements == NULL + ? 0 + : entry.data.sub_string.additional_elements->lv_len); + msgpack_pack_array(spacker, arr_size); + PACK_BIN(cstr_as_string(entry.data.sub_string.sub)); + DUMP_ADDITIONAL_ELEMENTS(entry.data.sub_string.additional_elements); + break; + } + case kSDItemSearchPattern: { + const size_t map_size = (size_t) ( + 1 // Search pattern is always present + + ONE_IF_NOT_DEFAULT(entry, search_pattern.magic) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.is_last_used) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.smartcase) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.has_line_offset) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.place_cursor_at_end) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.is_substitute_pattern) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.highlighted) + + ONE_IF_NOT_DEFAULT(entry, search_pattern.offset) + // finally, additional data: + + (size_t) ( + entry.data.search_pattern.additional_data + ? entry.data.search_pattern.additional_data->dv_hashtab.ht_used + : 0)); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(SEARCH_KEY_PAT); + PACK_BIN(cstr_as_string(entry.data.search_pattern.pat)); +#define PACK_BOOL(entry, name, attr) \ + do { \ + if (!CHECK_DEFAULT(entry, search_pattern.attr)) { \ + PACK_STATIC_STR(name); \ + if (sd_default_values[entry.type].data.search_pattern.attr) { \ + msgpack_pack_false(spacker); \ + } else { \ + msgpack_pack_true(spacker); \ + } \ + } \ + } while (0) + PACK_BOOL(entry, SEARCH_KEY_MAGIC, magic); + PACK_BOOL(entry, SEARCH_KEY_IS_LAST_USED, is_last_used); + PACK_BOOL(entry, SEARCH_KEY_SMARTCASE, smartcase); + PACK_BOOL(entry, SEARCH_KEY_HAS_LINE_OFFSET, has_line_offset); + PACK_BOOL(entry, SEARCH_KEY_PLACE_CURSOR_AT_END, place_cursor_at_end); + PACK_BOOL(entry, SEARCH_KEY_IS_SUBSTITUTE_PATTERN, is_substitute_pattern); + PACK_BOOL(entry, SEARCH_KEY_HIGHLIGHTED, highlighted); + if (!CHECK_DEFAULT(entry, search_pattern.offset)) { + PACK_STATIC_STR(SEARCH_KEY_OFFSET); + msgpack_pack_int64(spacker, entry.data.search_pattern.offset); + } +#undef PACK_BOOL + DUMP_ADDITIONAL_DATA(entry.data.search_pattern.additional_data); + break; + } + case kSDItemChange: + case kSDItemGlobalMark: + case kSDItemLocalMark: + case kSDItemJump: { + const size_t map_size = (size_t) ( + 1 // File name + + ONE_IF_NOT_DEFAULT(entry, filemark.mark.lnum) + + ONE_IF_NOT_DEFAULT(entry, filemark.mark.col) + + ONE_IF_NOT_DEFAULT(entry, filemark.name) + // Additional entries, if any: + + (size_t) ( + entry.data.filemark.additional_data == NULL + ? 0 + : entry.data.filemark.additional_data->dv_hashtab.ht_used)); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(KEY_FILE); + PACK_BIN(cstr_as_string(entry.data.filemark.fname)); + if (!CHECK_DEFAULT(entry, filemark.mark.lnum)) { + PACK_STATIC_STR(KEY_LNUM); + msgpack_pack_long(spacker, entry.data.filemark.mark.lnum); + } + if (!CHECK_DEFAULT(entry, filemark.mark.col)) { + PACK_STATIC_STR(KEY_COL); + msgpack_pack_long(spacker, entry.data.filemark.mark.col); + } + assert(entry.type == kSDItemJump || entry.type == kSDItemChange + ? CHECK_DEFAULT(entry, filemark.name) + : true); + if (!CHECK_DEFAULT(entry, filemark.name)) { + PACK_STATIC_STR(KEY_NAME_CHAR); + msgpack_pack_uint8(spacker, (uint8_t) entry.data.filemark.name); + } + DUMP_ADDITIONAL_DATA(entry.data.filemark.additional_data); + break; + } + case kSDItemRegister: { + const size_t map_size = (size_t) ( + 2 // Register contents and name + + ONE_IF_NOT_DEFAULT(entry, reg.type) + + ONE_IF_NOT_DEFAULT(entry, reg.width) + // Additional entries, if any: + + (size_t) (entry.data.reg.additional_data == NULL + ? 0 + : entry.data.reg.additional_data->dv_hashtab.ht_used)); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(REG_KEY_CONTENTS); + msgpack_pack_array(spacker, entry.data.reg.contents_size); + for (size_t i = 0; i < entry.data.reg.contents_size; i++) { + PACK_BIN(cstr_as_string(entry.data.reg.contents[i])); + } + PACK_STATIC_STR(KEY_NAME_CHAR); + msgpack_pack_char(spacker, entry.data.reg.name); + if (!CHECK_DEFAULT(entry, reg.type)) { + PACK_STATIC_STR(REG_KEY_TYPE); + msgpack_pack_uint8(spacker, entry.data.reg.type); + } + if (!CHECK_DEFAULT(entry, reg.width)) { + PACK_STATIC_STR(REG_KEY_WIDTH); + msgpack_pack_uint64(spacker, (uint64_t) entry.data.reg.width); + } + DUMP_ADDITIONAL_DATA(entry.data.reg.additional_data); + break; + } + case kSDItemBufferList: { + msgpack_pack_array(spacker, entry.data.buffer_list.size); + for (size_t i = 0; i < entry.data.buffer_list.size; i++) { + const size_t map_size = (size_t) ( + 1 // Buffer name + + (size_t) (entry.data.buffer_list.buffers[i].pos.lnum + != default_pos.lnum) + + (size_t) (entry.data.buffer_list.buffers[i].pos.col + != default_pos.col) + // Additional entries, if any: + + (size_t) ( + entry.data.buffer_list.buffers[i].additional_data == NULL + ? 0 + : (entry.data.buffer_list.buffers[i].additional_data + ->dv_hashtab.ht_used))); + msgpack_pack_map(spacker, map_size); + PACK_STATIC_STR(KEY_FILE); + PACK_BIN(cstr_as_string(entry.data.buffer_list.buffers[i].fname)); + if (entry.data.buffer_list.buffers[i].pos.lnum != 1) { + PACK_STATIC_STR(KEY_LNUM); + msgpack_pack_uint64( + spacker, (uint64_t) entry.data.buffer_list.buffers[i].pos.lnum); + } + if (entry.data.buffer_list.buffers[i].pos.col != 0) { + PACK_STATIC_STR(KEY_COL); + msgpack_pack_uint64( + spacker, (uint64_t) entry.data.buffer_list.buffers[i].pos.col); + } + DUMP_ADDITIONAL_DATA(entry.data.buffer_list.buffers[i].additional_data); + } + break; + } + case kSDItemHeader: { + msgpack_pack_map(spacker, entry.data.header.size); + for (size_t i = 0; i < entry.data.header.size; i++) { + PACK_STRING(entry.data.header.items[i].key); + const Object obj = entry.data.header.items[i].value; + switch (obj.type) { + case kObjectTypeString: { + PACK_BIN(obj.data.string); + break; + } + case kObjectTypeInteger: { + msgpack_pack_int64(spacker, (int64_t) obj.data.integer); + break; + } + default: { + assert(false); + } + } + } + break; + } + } +#undef CHECK_DEFAULT +#undef ONE_IF_NOT_DEFAULT + if (!max_kbyte || sbuf.size <= max_kbyte * 1024) { + if (entry.type == kSDItemUnknown) { + if (msgpack_pack_uint64(packer, entry.data.unknown_item.type) == -1) { + goto shada_pack_entry_error; + } + } else { + if (msgpack_pack_uint64(packer, (uint64_t) entry.type) == -1) { + goto shada_pack_entry_error; + } + } + if (msgpack_pack_uint64(packer, (uint64_t) entry.timestamp) == -1) { + goto shada_pack_entry_error; + } + if (sbuf.size > 0) { + if ((msgpack_pack_uint64(packer, (uint64_t) sbuf.size) == -1) + || (packer->callback(packer->data, sbuf.data, + (unsigned) sbuf.size) == -1)) { + goto shada_pack_entry_error; + } + } + } + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); + return true; +shada_pack_entry_error: + msgpack_packer_free(spacker); + msgpack_sbuffer_destroy(&sbuf); + return false; +} +#undef PACK_STRING + +/// Write single ShaDa entry, converting it if needed +/// +/// @warning Frees entry after packing. +/// +/// @param[in] packer Packer used to write entry. +/// @param[in] sd_conv Conversion definitions. +/// @param[in] entry Entry written. If entry.can_free_entry is false then +/// it assumes that entry was not converted, otherwise it +/// is assumed that entry was already converted. +/// @param[in] max_kbyte Maximum size of an item in KiB. Zero means no +/// restrictions. +static bool shada_pack_encoded_entry(msgpack_packer *const packer, + const vimconv_T *const sd_conv, + PossiblyFreedShadaEntry entry, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL +{ + bool ret = true; + if (entry.can_free_entry) { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + shada_free_shada_entry(&entry.data); + return ret; + } +#define RUN_WITH_CONVERTED_STRING(cstr, code) \ + do { \ + bool did_convert = false; \ + if (sd_conv->vc_type != CONV_NONE && has_non_ascii((cstr))) { \ + char *const converted_string = string_convert(sd_conv, (cstr), NULL); \ + if (converted_string != NULL) { \ + (cstr) = converted_string; \ + did_convert = true; \ + } \ + } \ + code \ + if (did_convert) { \ + xfree((cstr)); \ + } \ + } while (0) + switch (entry.data.type) { + case kSDItemUnknown: + case kSDItemMissing: { + assert(false); + } + case kSDItemSearchPattern: { + RUN_WITH_CONVERTED_STRING(entry.data.data.search_pattern.pat, { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + }); + break; + } + case kSDItemHistoryEntry: { + RUN_WITH_CONVERTED_STRING(entry.data.data.history_item.string, { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + }); + break; + } + case kSDItemSubString: { + RUN_WITH_CONVERTED_STRING(entry.data.data.sub_string.sub, { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + }); + break; + } + case kSDItemVariable: { + if (sd_conv->vc_type != CONV_NONE) { + typval_T tgttv; + var_item_copy(sd_conv, &entry.data.data.global_var.value, &tgttv, + true, 0); + clear_tv(&entry.data.data.global_var.value); + entry.data.data.global_var.value = tgttv; + } + ret = shada_pack_entry(packer, entry.data, max_kbyte); + break; + } + case kSDItemRegister: { + bool did_convert = false; + if (sd_conv->vc_type != CONV_NONE) { + size_t first_non_ascii = 0; + for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { + if (has_non_ascii(entry.data.data.reg.contents[i])) { + first_non_ascii = i; + did_convert = true; + break; + } + } + if (did_convert) { + entry.data.data.reg.contents = + xmemdup(entry.data.data.reg.contents, + (entry.data.data.reg.contents_size + * sizeof(entry.data.data.reg.contents))); + for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { + if (i >= first_non_ascii) { + entry.data.data.reg.contents[i] = get_converted_string( + sd_conv, + entry.data.data.reg.contents[i], + strlen(entry.data.data.reg.contents[i])); + } else { + entry.data.data.reg.contents[i] = + xstrdup(entry.data.data.reg.contents[i]); + } + } + } + } + ret = shada_pack_entry(packer, entry.data, max_kbyte); + if (did_convert) { + for (size_t i = 0; i < entry.data.data.reg.contents_size; i++) { + xfree(entry.data.data.reg.contents[i]); + } + xfree(entry.data.data.reg.contents); + } + break; + } + case kSDItemHeader: + case kSDItemGlobalMark: + case kSDItemJump: + case kSDItemBufferList: + case kSDItemLocalMark: + case kSDItemChange: { + ret = shada_pack_entry(packer, entry.data, max_kbyte); + break; + } + } +#undef RUN_WITH_CONVERTED_STRING + return ret; +} + +/// Compare two FileMarks structure to order them by greatest_timestamp +/// +/// Order is reversed: structure with greatest greatest_timestamp comes first. +/// Function signature is compatible with qsort. +static int compare_file_marks(const void *a, const void *b) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + const FileMarks *const *const a_fms = a; + const FileMarks *const *const b_fms = b; + return ((*a_fms)->greatest_timestamp == (*b_fms)->greatest_timestamp + ? 0 + : ((*a_fms)->greatest_timestamp > (*b_fms)->greatest_timestamp + ? -1 + : 1)); +} + +/// Parse msgpack object that has given length +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[in] length Object length. +/// @param[out] ret_unpacked Location where read result should be saved. If +/// NULL then unpacked data will be freed. Must be +/// NULL if `ret_buf` is NULL. +/// @param[out] ret_buf Buffer containing parsed string. +/// +/// @return kSDReadStatusNotShaDa, kSDReadStatusReadError or +/// kSDReadStatusSuccess. +static inline ShaDaReadResult shada_parse_msgpack( + ShaDaReadDef *const sd_reader, const size_t length, + msgpack_unpacked *ret_unpacked, char **const ret_buf) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_ARG(1) +{ + const uintmax_t initial_fpos = sd_reader->fpos; + char *const buf = xmalloc(length); + + const ShaDaReadResult fl_ret = fread_len(sd_reader, buf, length); + if (fl_ret != kSDReadStatusSuccess) { + xfree(buf); + return fl_ret; + } + bool did_try_to_free = false; +shada_parse_msgpack_read_next: {} + size_t off = 0; + msgpack_unpacked unpacked; + msgpack_unpacked_init(&unpacked); + const msgpack_unpack_return result = + msgpack_unpack_next(&unpacked, buf, length, &off); + ShaDaReadResult ret = kSDReadStatusSuccess; + switch (result) { + case MSGPACK_UNPACK_SUCCESS: { + if (off < length) { + goto shada_parse_msgpack_extra_bytes; + } + break; + } + case MSGPACK_UNPACK_PARSE_ERROR: { + emsgu(_(RCERR "Failed to parse ShaDa file due to a msgpack parser error " + "at position %" PRIu64), + (uint64_t) initial_fpos); + ret = kSDReadStatusNotShaDa; + break; + } + case MSGPACK_UNPACK_NOMEM_ERROR: { + if (!did_try_to_free) { + did_try_to_free = true; + try_to_free_memory(); + goto shada_parse_msgpack_read_next; + } + EMSG(_(e_outofmem)); + ret = kSDReadStatusReadError; + break; + } + case MSGPACK_UNPACK_CONTINUE: { + emsgu(_(RCERR "Failed to parse ShaDa file: incomplete msgpack string " + "at position %" PRIu64), + (uint64_t) initial_fpos); + ret = kSDReadStatusNotShaDa; + break; + } + case MSGPACK_UNPACK_EXTRA_BYTES: { +shada_parse_msgpack_extra_bytes: + emsgu(_(RCERR "Failed to parse ShaDa file: extra bytes in msgpack string " + "at position %" PRIu64), + (uint64_t) initial_fpos); + ret = kSDReadStatusNotShaDa; + break; + } + } + if (ret_buf != NULL && ret == kSDReadStatusSuccess) { + if (ret_unpacked == NULL) { + msgpack_unpacked_destroy(&unpacked); + } else { + *ret_unpacked = unpacked; + } + *ret_buf = buf; + } else { + assert(ret_buf == NULL || ret != kSDReadStatusSuccess); + msgpack_unpacked_destroy(&unpacked); + xfree(buf); + } + return ret; +} + +/// Read and merge in ShaDa file, used when writing +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[in] srni_flags Flags determining what to read. +/// @param[in] max_kbyte Maximum size of one element. +/// @param[in,out] ret_wms Location where results are saved. +/// @param[out] packer MessagePack packer for entries which are not +/// merged. +static inline ShaDaWriteResult shada_read_when_writing( + ShaDaReadDef *const sd_reader, const unsigned srni_flags, + const size_t max_kbyte, WriteMergerState *const wms, + msgpack_packer *const packer) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + ShaDaWriteResult ret = kSDWriteSuccessfull; + ShadaEntry entry; + ShaDaReadResult srni_ret; + while ((srni_ret = shada_read_next_item(sd_reader, &entry, srni_flags, + max_kbyte)) + != kSDReadStatusFinished) { + switch (srni_ret) { + case kSDReadStatusSuccess: { + break; + } + case kSDReadStatusFinished: { + // Should be handled by the while condition. + assert(false); + } + case kSDReadStatusNotShaDa: { + ret = kSDWriteReadNotShada; + // fallthrough + } + case kSDReadStatusReadError: { + return ret; + } + case kSDReadStatusMalformed: { + continue; + } + } +#define COMPARE_WITH_ENTRY(wms_entry_, entry) \ + do { \ + PossiblyFreedShadaEntry *const wms_entry = (wms_entry_); \ + if (wms_entry->data.type != kSDItemMissing) { \ + if (wms_entry->data.timestamp >= (entry).timestamp) { \ + shada_free_shada_entry(&(entry)); \ + break; \ + } \ + if (wms_entry->can_free_entry) { \ + shada_free_shada_entry(&wms_entry->data); \ + } \ + } \ + wms_entry->can_free_entry = true; \ + wms_entry->data = (entry); \ + } while (0) + switch (entry.type) { + case kSDItemMissing: { + break; + } + case kSDItemHeader: + case kSDItemBufferList: { + assert(false); + } + case kSDItemUnknown: { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + case kSDItemSearchPattern: { + COMPARE_WITH_ENTRY((entry.data.search_pattern.is_substitute_pattern + ? &wms->sub_search_pattern + : &wms->search_pattern), entry); + break; + } + case kSDItemSubString: { + COMPARE_WITH_ENTRY(&wms->replacement, entry); + break; + } + case kSDItemHistoryEntry: { + if (entry.data.history_item.histtype >= HIST_COUNT) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + hms_insert(&wms->hms[entry.data.history_item.histtype], entry, true, + true); + break; + } + case kSDItemRegister: { + const int idx = op_reg_index(entry.data.reg.name); + if (idx < 0) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + COMPARE_WITH_ENTRY(&wms->registers[idx], entry); + break; + } + case kSDItemVariable: { + if (!in_strset(&wms->dumped_variables, entry.data.global_var.name)) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + } + shada_free_shada_entry(&entry); + break; + } + case kSDItemGlobalMark: { + const int idx = mark_global_index(entry.data.filemark.name); + if (idx < 0) { + if (!shada_pack_entry(packer, entry, 0)) { + ret = kSDWriteFailed; + } + shada_free_shada_entry(&entry); + break; + } + COMPARE_WITH_ENTRY(&wms->global_marks[idx], entry); + break; + } + case kSDItemChange: + case kSDItemLocalMark: { + if (shada_removable(entry.data.filemark.fname)) { + shada_free_shada_entry(&entry); + break; + } + const char *const fname = (const char *) entry.data.filemark.fname; + khiter_t k; + int kh_ret; + k = kh_put(file_marks, &wms->file_marks, fname, &kh_ret); + FileMarks *const filemarks = &kh_val(&wms->file_marks, k); + if (kh_ret > 0) { + memset(filemarks, 0, sizeof(*filemarks)); + } + if (entry.timestamp > filemarks->greatest_timestamp) { + filemarks->greatest_timestamp = entry.timestamp; + } + if (entry.type == kSDItemLocalMark) { + const int idx = mark_local_index(entry.data.filemark.name); + if (idx < 0) { + filemarks->additional_marks = xrealloc( + filemarks->additional_marks, + (++filemarks->additional_marks_size + * sizeof(filemarks->additional_marks[0]))); + filemarks->additional_marks[filemarks->additional_marks_size - 1] = + entry; + } else { + PossiblyFreedShadaEntry *const wms_entry = &filemarks->marks[idx]; + if (wms_entry->data.type != kSDItemMissing) { + if (wms_entry->data.timestamp >= entry.timestamp) { + shada_free_shada_entry(&entry); + break; + } + if (wms_entry->can_free_entry) { + if (kh_key(&wms->file_marks, k) + == wms_entry->data.data.filemark.fname) { + kh_key(&wms->file_marks, k) = entry.data.filemark.fname; + } + shada_free_shada_entry(&wms_entry->data); + } + } + wms_entry->can_free_entry = true; + wms_entry->data = entry; + } + } else { +#define FREE_POSSIBLY_FREED_SHADA_ENTRY(entry) \ + do { \ + if (entry.can_free_entry) { \ + shada_free_shada_entry(&entry.data); \ + } \ + } while (0) +#define SDE_TO_PFSDE(entry) \ + ((PossiblyFreedShadaEntry) { .can_free_entry = true, .data = entry }) +#define AFTERFREE_DUMMY(entry) +#define DUMMY_IDX_ADJ(i) + MERGE_JUMPS(filemarks->changes_size, filemarks->changes, + PossiblyFreedShadaEntry, data.timestamp, + data.data.filemark.mark, entry, true, + FREE_POSSIBLY_FREED_SHADA_ENTRY, SDE_TO_PFSDE, + DUMMY_IDX_ADJ, AFTERFREE_DUMMY); + } + break; + } + case kSDItemJump: { + MERGE_JUMPS(wms->jumps_size, wms->jumps, PossiblyFreedShadaEntry, + data.timestamp, data.data.filemark.mark, entry, + strcmp(jl_entry.data.data.filemark.fname, + entry.data.filemark.fname) == 0, + FREE_POSSIBLY_FREED_SHADA_ENTRY, SDE_TO_PFSDE, + DUMMY_IDX_ADJ, AFTERFREE_DUMMY); +#undef FREE_POSSIBLY_FREED_SHADA_ENTRY +#undef SDE_TO_PFSDE +#undef DUMMY_IDX_ADJ +#undef AFTERFREE_DUMMY + break; + } + } + } +#undef COMPARE_WITH_ENTRY + return ret; +} + +/// Write ShaDa file +/// +/// @param[in] sd_writer Structure containing file writer definition. +/// @param[in] sd_reader Structure containing file reader definition. If it is +/// not NULL then contents of this file will be merged +/// with current Neovim runtime. +static ShaDaWriteResult shada_write(ShaDaWriteDef *const sd_writer, + ShaDaReadDef *const sd_reader) + FUNC_ATTR_NONNULL_ARG(1) +{ + ShaDaWriteResult ret = kSDWriteSuccessfull; + int max_kbyte_i = get_shada_parameter('s'); + if (max_kbyte_i < 0) { + max_kbyte_i = 10; + } + if (max_kbyte_i == 0) { + return ret; + } + + WriteMergerState *const wms = xcalloc(1, sizeof(*wms)); + bool dump_one_history[HIST_COUNT]; + const bool dump_global_vars = (find_shada_parameter('!') != NULL); + int max_reg_lines = get_shada_parameter('<'); + if (max_reg_lines < 0) { + max_reg_lines = get_shada_parameter('"'); + } + const bool limit_reg_lines = max_reg_lines >= 0; + const bool dump_registers = (max_reg_lines != 0); + khash_t(bufset) removable_bufs = KHASH_EMPTY_TABLE(bufset); + const size_t max_kbyte = (size_t) max_kbyte_i; + const size_t num_marked_files = (size_t) get_shada_parameter('\''); + const bool dump_global_marks = get_shada_parameter('f') != 0; + bool dump_history = false; + + // Initialize history merger + for (uint8_t i = 0; i < HIST_COUNT; i++) { + long num_saved = get_shada_parameter(hist_type2char(i)); + if (num_saved == -1) { + num_saved = p_hi; + } + if (num_saved > 0) { + dump_history = true; + dump_one_history[i] = true; + hms_init(&wms->hms[i], i, (size_t) num_saved, sd_reader != NULL, false); + } else { + dump_one_history[i] = false; + } + } + + const unsigned srni_flags = (unsigned) ( + kSDReadUndisableableData + | kSDReadUnknown + | (dump_history ? kSDReadHistory : 0) + | (dump_registers ? kSDReadRegisters : 0) + | (dump_global_vars ? kSDReadVariables : 0) + | (dump_global_marks ? kSDReadGlobalMarks : 0) + | (num_marked_files ? kSDReadLocalMarks | kSDReadChanges : 0)); + + msgpack_packer *const packer = msgpack_packer_new(sd_writer, + &msgpack_sd_writer_write); + + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL && shada_removable((char *) buf->b_ffname)) { + int kh_ret; + (void) kh_put(bufset, &removable_bufs, (uintptr_t) buf, &kh_ret); + } + } + + // Write header + if (!shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemHeader, + .timestamp = os_time(), + .data = { + .header = { + .size = 5, + .capacity = 5, + .items = ((KeyValuePair[]) { + { STATIC_CSTR_AS_STRING("generator"), + STRING_OBJ(STATIC_CSTR_AS_STRING("nvim")) }, + { STATIC_CSTR_AS_STRING("version"), + STRING_OBJ(cstr_as_string(longVersion)) }, + { STATIC_CSTR_AS_STRING("max_kbyte"), + INTEGER_OBJ((Integer) max_kbyte) }, + { STATIC_CSTR_AS_STRING("pid"), + INTEGER_OBJ((Integer) os_get_pid()) }, + { STATIC_CSTR_AS_STRING("encoding"), + STRING_OBJ(cstr_as_string((char *) p_enc)) }, + }), + } + } + }, 0)) { + ret = kSDWriteFailed; + goto shada_write_exit; + } + + // Write buffer list + if (find_shada_parameter('%') != NULL) { + size_t buf_count = 0; + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname != NULL && !in_bufset(&removable_bufs, buf)) { + buf_count++; + } + } + + ShadaEntry buflist_entry = (ShadaEntry) { + .type = kSDItemBufferList, + .timestamp = os_time(), + .data = { + .buffer_list = { + .size = buf_count, + .buffers = xmalloc(buf_count + * sizeof(*buflist_entry.data.buffer_list.buffers)), + }, + }, + }; + size_t i = 0; + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL || in_bufset(&removable_bufs, buf)) { + continue; + } + buflist_entry.data.buffer_list.buffers[i] = (struct buffer_list_buffer) { + .pos = buf->b_last_cursor.mark, + .fname = (char *) buf->b_ffname, + .additional_data = buf->additional_data, + }; + i++; + } + if (!shada_pack_entry(packer, buflist_entry, 0)) { + xfree(buflist_entry.data.buffer_list.buffers); + ret = kSDWriteFailed; + goto shada_write_exit; + } + xfree(buflist_entry.data.buffer_list.buffers); + } + + // Write some of the variables + if (dump_global_vars) { + const void *var_iter = NULL; + const Timestamp cur_timestamp = os_time(); + do { + typval_T vartv; + const char *name = NULL; + var_iter = var_shada_iter(var_iter, &name, &vartv); + if (name == NULL) { + break; + } + typval_T tgttv; + if (sd_writer->sd_conv.vc_type != CONV_NONE) { + var_item_copy(&sd_writer->sd_conv, &vartv, &tgttv, true, 0); + } else { + copy_tv(&vartv, &tgttv); + } + if (!shada_pack_entry(packer, (ShadaEntry) { + .type = kSDItemVariable, + .timestamp = cur_timestamp, + .data = { + .global_var = { + .name = (char *) name, + .value = tgttv, + .additional_elements = NULL, + } + } + }, max_kbyte)) { + clear_tv(&vartv); + clear_tv(&tgttv); + ret = kSDWriteFailed; + goto shada_write_exit; + } + clear_tv(&vartv); + clear_tv(&tgttv); + int kh_ret; + (void) kh_put(strset, &wms->dumped_variables, name, &kh_ret); + } while (var_iter != NULL); + } + + const bool search_highlighted = !(no_hlsearch + || find_shada_parameter('h') != NULL); + const bool search_last_used = search_was_last_used(); +#define ADD_SEARCH_PAT(func, wms_attr, hlo, pcae, o, is_sub) \ + do { \ + SearchPattern pat; \ + func(&pat); \ + if (pat.pat != NULL) { \ + wms->wms_attr = (PossiblyFreedShadaEntry) { \ + .can_free_entry = false, \ + .data = { \ + .type = kSDItemSearchPattern, \ + .timestamp = pat.timestamp, \ + .data = { \ + .search_pattern = { \ + .magic = pat.magic, \ + .smartcase = !pat.no_scs, \ + .has_line_offset = hlo, \ + .place_cursor_at_end = pcae, \ + .offset = o, \ + .is_last_used = (is_sub ^ search_last_used), \ + .is_substitute_pattern = is_sub, \ + .highlighted = ((is_sub ^ search_last_used) \ + && search_highlighted), \ + .pat = (char *) pat.pat, \ + .additional_data = pat.additional_data, \ + } \ + } \ + } \ + }; \ + } \ + } while (0) + + // Initialize search pattern + ADD_SEARCH_PAT(get_search_pattern, search_pattern, pat.off.line, \ + pat.off.end, pat.off.off, false); + + // Initialize substitute search pattern + ADD_SEARCH_PAT(get_substitute_pattern, sub_search_pattern, false, false, 0, + true); +#undef ADD_SEARCH_PAT + + // Initialize substitute replacement string + { + SubReplacementString sub; + sub_get_replacement(&sub); + wms->replacement = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemSubString, + .timestamp = sub.timestamp, + .data = { + .sub_string = { + .sub = (char *) sub.sub, + .additional_elements = sub.additional_elements, + } + } + } + }; + } + + // Initialize jump list + const void *jump_iter = NULL; + do { + xfmark_T fm; + cleanup_jumplist(); + jump_iter = mark_jumplist_iter(jump_iter, curwin, &fm); + const buf_T *const buf = (fm.fmark.fnum == 0 + ? NULL + : buflist_findnr(fm.fmark.fnum)); + if (buf != NULL + ? in_bufset(&removable_bufs, buf) + : fm.fmark.fnum != 0) { + continue; + } + const char *const fname = (char *) (fm.fmark.fnum == 0 + ? (fm.fname == NULL ? NULL : fm.fname) + : buf->b_ffname); + if (fname == NULL) { + continue; + } + wms->jumps[wms->jumps_size++] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemJump, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .name = NUL, + .mark = fm.fmark.mark, + .fname = (char *) fname, + .additional_data = fm.fmark.additional_data, + } + } + } + }; + } while (jump_iter != NULL); + + // Initialize global marks + if (dump_global_marks) { + const void *global_mark_iter = NULL; + do { + char name = NUL; + xfmark_T fm; + global_mark_iter = mark_global_iter(global_mark_iter, &name, &fm); + if (name == NUL) { + break; + } + const char *fname; + if (fm.fmark.fnum == 0) { + assert(fm.fname != NULL); + if (shada_removable((const char *) fm.fname)) { + continue; + } + fname = (const char *) fm.fname; + } else { + const buf_T *const buf = buflist_findnr(fm.fmark.fnum); + if (buf == NULL || buf->b_ffname == NULL + || in_bufset(&removable_bufs, buf)) { + continue; + } + fname = (const char *) buf->b_ffname; + } + wms->global_marks[mark_global_index(name)] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemGlobalMark, + .timestamp = fm.fmark.timestamp, + .data = { + .filemark = { + .mark = fm.fmark.mark, + .name = name, + .additional_data = fm.fmark.additional_data, + .fname = (char *) fname, + } + } + }, + }; + } while (global_mark_iter != NULL); + } + + // Initialize registers + if (dump_registers) { + const void *reg_iter = NULL; + do { + yankreg_T reg; + char name = NUL; + reg_iter = op_register_iter(reg_iter, &name, ®); + if (name == NUL) { + break; + } + if (limit_reg_lines && reg.y_size > max_reg_lines) { + continue; + } + wms->registers[op_reg_index(name)] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemRegister, + .timestamp = reg.timestamp, + .data = { + .reg = { + .contents = (char **) reg.y_array, + .contents_size = (size_t) reg.y_size, + .type = (uint8_t) reg.y_type, + .width = (size_t) (reg.y_type == MBLOCK ? reg.y_width : 0), + .additional_data = reg.additional_data, + .name = name, + } + } + } + }; + } while (reg_iter != NULL); + } + + // Initialize buffers + if (num_marked_files > 0) { + FOR_ALL_BUFFERS(buf) { + if (buf->b_ffname == NULL || in_bufset(&removable_bufs, buf)) { + continue; + } + const void *local_marks_iter = NULL; + const char *const fname = (const char *) buf->b_ffname; + khiter_t k; + int kh_ret; + k = kh_put(file_marks, &wms->file_marks, fname, &kh_ret); + FileMarks *const filemarks = &kh_val(&wms->file_marks, k); + if (kh_ret > 0) { + memset(filemarks, 0, sizeof(*filemarks)); + } + do { + fmark_T fm; + char name = NUL; + local_marks_iter = mark_buffer_iter(local_marks_iter, buf, &name, &fm); + if (name == NUL) { + break; + } + filemarks->marks[mark_local_index(name)] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemLocalMark, + .timestamp = fm.timestamp, + .data = { + .filemark = { + .mark = fm.mark, + .name = name, + .fname = (char *) fname, + .additional_data = fm.additional_data, + } + } + } + }; + if (fm.timestamp > filemarks->greatest_timestamp) { + filemarks->greatest_timestamp = fm.timestamp; + } + } while (local_marks_iter != NULL); + for (int i = 0; i < buf->b_changelistlen; i++) { + const fmark_T fm = buf->b_changelist[i]; + filemarks->changes[i] = (PossiblyFreedShadaEntry) { + .can_free_entry = false, + .data = { + .type = kSDItemChange, + .timestamp = fm.timestamp, + .data = { + .filemark = { + .mark = fm.mark, + .fname = (char *) fname, + .additional_data = fm.additional_data, + } + } + } + }; + if (fm.timestamp > filemarks->greatest_timestamp) { + filemarks->greatest_timestamp = fm.timestamp; + } + } + filemarks->changes_size = (size_t) buf->b_changelistlen; + } + } + + if (sd_reader != NULL) { + const ShaDaWriteResult srww_ret = shada_read_when_writing( + sd_reader, srni_flags, max_kbyte, wms, packer); + if (srww_ret != kSDWriteSuccessfull) { + ret = srww_ret; + } + } + + // Write the rest +#define PACK_WMS_ARRAY(wms_array) \ + do { \ + for (size_t i_ = 0; i_ < ARRAY_SIZE(wms_array); i_++) { \ + if (wms_array[i_].data.type != kSDItemMissing) { \ + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, \ + wms_array[i_], \ + max_kbyte)) { \ + ret = kSDWriteFailed; \ + goto shada_write_exit; \ + } \ + } \ + } \ + } while (0) + PACK_WMS_ARRAY(wms->global_marks); + PACK_WMS_ARRAY(wms->registers); + for (size_t i = 0; i < wms->jumps_size; i++) { + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, wms->jumps[i], + max_kbyte)) { + ret = kSDWriteFailed; + goto shada_write_exit; + } + } +#define PACK_WMS_ENTRY(wms_entry) \ + do { \ + if (wms_entry.data.type != kSDItemMissing) { \ + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, wms_entry, \ + max_kbyte)) { \ + ret = kSDWriteFailed; \ + goto shada_write_exit; \ + } \ + } \ + } while (0) + PACK_WMS_ENTRY(wms->search_pattern); + PACK_WMS_ENTRY(wms->sub_search_pattern); + PACK_WMS_ENTRY(wms->replacement); +#undef PACK_WMS_ENTRY + + const size_t file_markss_size = kh_size(&wms->file_marks); + FileMarks **const all_file_markss = + xmalloc(file_markss_size * sizeof(*all_file_markss)); + FileMarks **cur_file_marks = all_file_markss; + for (khint_t i = kh_begin(&wms->file_marks); i != kh_end(&wms->file_marks); + i++) { + if (kh_exist(&wms->file_marks, i)) { + *cur_file_marks++ = &kh_val(&wms->file_marks, i); + } + } + qsort((void *) all_file_markss, file_markss_size, sizeof(*all_file_markss), + &compare_file_marks); + const size_t file_markss_to_dump = MIN(num_marked_files, file_markss_size); + for (size_t i = 0; i < file_markss_to_dump; i++) { + PACK_WMS_ARRAY(all_file_markss[i]->marks); + for (size_t j = 0; j < all_file_markss[i]->changes_size; j++) { + if (!shada_pack_encoded_entry(packer, &sd_writer->sd_conv, + all_file_markss[i]->changes[j], + max_kbyte)) { + ret = kSDWriteFailed; + goto shada_write_exit; + } + } + for (size_t j = 0; j < all_file_markss[i]->additional_marks_size; j++) { + if (!shada_pack_entry(packer, all_file_markss[i]->additional_marks[j], + 0)) { + shada_free_shada_entry(&all_file_markss[i]->additional_marks[j]); + ret = kSDWriteFailed; + goto shada_write_exit; + } + shada_free_shada_entry(&all_file_markss[i]->additional_marks[j]); + } + xfree(all_file_markss[i]->additional_marks); + } + xfree(all_file_markss); +#undef PACK_WMS_ARRAY + + if (dump_history) { + for (size_t i = 0; i < HIST_COUNT; i++) { + if (dump_one_history[i]) { + hms_insert_whole_neovim_history(&wms->hms[i]); + HMS_ITER(&wms->hms[i], cur_entry) { + if (!shada_pack_encoded_entry( + packer, &sd_writer->sd_conv, (PossiblyFreedShadaEntry) { + .data = cur_entry->data, + .can_free_entry = cur_entry->can_free_entry, + }, max_kbyte)) { + ret = kSDWriteFailed; + break; + } + } + hms_dealloc(&wms->hms[i]); + if (ret == kSDWriteFailed) { + goto shada_write_exit; + } + } + } + } + +shada_write_exit: + kh_dealloc(file_marks, &wms->file_marks); + kh_dealloc(bufset, &removable_bufs); + msgpack_packer_free(packer); + kh_dealloc(strset, &wms->dumped_variables); + xfree(wms); + return ret; +} + +#undef PACK_STATIC_STR + +/// Write ShaDa file to a given location +/// +/// @param[in] fname File to write to. If it is NULL or empty then default +/// location is used. +/// @param[in] nomerge If true then old file is ignored. +/// +/// @return OK if writing was successfull, FAIL otherwise. +int shada_write_file(const char *const file, bool nomerge) +{ + if (shada_disabled()) { + return FAIL; + } + + char *const fname = shada_filename(file); + char *tempname = NULL; + ShaDaWriteDef sd_writer = (ShaDaWriteDef) { + .write = &write_file, + .close = &close_sd_writer, + .error = NULL, + }; + ShaDaReadDef sd_reader; + + intptr_t fd; + + if (!nomerge) { + if (open_shada_file_for_reading(fname, &sd_reader) != 0) { + nomerge = true; + goto shada_write_file_nomerge; + } + tempname = modname(fname, ".tmp.a", false); + if (tempname == NULL) { + nomerge = true; + goto shada_write_file_nomerge; + } + + // Save permissions from the original file, with modifications: + int perm = (int) os_getperm(fname); + perm = (perm >= 0) ? ((perm & 0777) | 0600) : 0600; + // ^3 ^1 ^2 ^2,3 + // 1: Strip SUID bit if any. + // 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 == EEXIST +#ifdef ELOOP + || -fd == ELOOP +#endif + ) { + // File already exists, try another name + char *const wp = tempname + strlen(tempname) - 1; + if (*wp == 'z') { + // Tried names from .tmp.a to .tmp.z, all failed. Something must be + // wrong then. + EMSG2(_("E138: All %s.tmp.X files exist, cannot write ShaDa file!"), + fname); + xfree(fname); + xfree(tempname); + return FAIL; + } else { + (*wp)++; + goto shada_write_file_open; + } + } + } + } + if (nomerge) { +shada_write_file_nomerge: {} + char *const tail = path_tail_with_sep(fname); + if (tail != fname) { + const char tail_save = *tail; + *tail = NUL; + if (!os_isdir(fname)) { + int ret; + char *failed_dir; + if ((ret = os_mkdir_recurse(fname, 0700, &failed_dir)) != 0) { + EMSG3(_(SERR "Failed to create directory %s " + "for writing ShaDa file: %s"), + failed_dir, os_strerror(ret)); + xfree(fname); + xfree(failed_dir); + return FAIL; + } + } + *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(); + } + + if (fd < 0) { + xfree(fname); + xfree(tempname); + return FAIL; + } + + sd_writer.cookie = (void *) fd; + + convert_setup(&sd_writer.sd_conv, p_enc, "utf-8"); + + const ShaDaWriteResult sw_ret = shada_write(&sd_writer, (nomerge + ? NULL + : &sd_reader)); +#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. + FileInfo old_info; + if (os_fileinfo((char *)fname, &old_info)) { + 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); + 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) + : (old_info.stat.st_gid == getgid() + ? (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!"), + tempname, fname); + } else { + did_remove = true; + os_remove(tempname); + } + } else { + if (sw_ret == kSDWriteReadNotShada) { + EMSG3(_(RNERR "Did not rename %s because %s " + "does not looks like a ShaDa file"), tempname, fname); + } else { + EMSG3(_(RNERR "Did not rename %s to %s because there were errors " + "during writing it"), tempname, fname); + } + } + if (!did_remove) { +#ifdef UNIX +shada_write_file_did_not_remove: +#endif + EMSG3(_(RNERR "Do not forget to remove %s or rename it manually to %s."), + tempname, fname); + } + xfree(tempname); + } + + xfree(fname); + return OK; +} + +/// Read marks information from ShaDa file +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_marks(void) +{ + return shada_read_file(NULL, kShaDaWantMarks); +} + +/// Read all information from ShaDa file +/// +/// @param[in] fname File to write to. If it is NULL or empty then default +/// @param[in] forceit If true, use forced reading (prioritize file contents +/// over current Neovim state). +/// @param[in] missing_ok If true, do not error out when file is missing. +/// +/// @return OK in case of success, FAIL otherwise. +int shada_read_everything(const char *const fname, const bool forceit, + const bool missing_ok) +{ + return shada_read_file(fname, + kShaDaWantInfo|kShaDaWantMarks|kShaDaGetOldfiles + |(forceit?kShaDaForceit:0) + |(missing_ok?0:kShaDaMissingError)); +} + +static void shada_free_shada_entry(ShadaEntry *const entry) +{ + if (entry == NULL) { + return; + } + switch (entry->type) { + case kSDItemMissing: { + break; + } + case kSDItemUnknown: { + xfree(entry->data.unknown_item.contents); + break; + } + case kSDItemHeader: { + api_free_dictionary(entry->data.header); + break; + } + case kSDItemChange: + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + dict_unref(entry->data.filemark.additional_data); + xfree(entry->data.filemark.fname); + break; + } + case kSDItemSearchPattern: { + dict_unref(entry->data.search_pattern.additional_data); + xfree(entry->data.search_pattern.pat); + break; + } + case kSDItemRegister: { + dict_unref(entry->data.reg.additional_data); + for (size_t i = 0; i < entry->data.reg.contents_size; i++) { + xfree(entry->data.reg.contents[i]); + } + xfree(entry->data.reg.contents); + break; + } + case kSDItemHistoryEntry: { + list_unref(entry->data.history_item.additional_elements); + xfree(entry->data.history_item.string); + break; + } + case kSDItemVariable: { + list_unref(entry->data.global_var.additional_elements); + xfree(entry->data.global_var.name); + clear_tv(&entry->data.global_var.value); + break; + } + case kSDItemSubString: { + list_unref(entry->data.sub_string.additional_elements); + xfree(entry->data.sub_string.sub); + break; + } + case kSDItemBufferList: { + for (size_t i = 0; i < entry->data.buffer_list.size; i++) { + xfree(entry->data.buffer_list.buffers[i].fname); + dict_unref(entry->data.buffer_list.buffers[i].additional_data); + } + xfree(entry->data.buffer_list.buffers); + break; + } + } +} + +#ifndef HAVE_BE64TOH +static inline uint64_t be64toh(uint64_t big_endian_64_bits) +{ +#ifdef ORDER_BIG_ENDIAN + return big_endian_64_bits; +#else + // It may appear that when !defined(ORDER_BIG_ENDIAN) actual order is big + // endian. This variant is suboptimal, but it works regardless of actual + // order. + uint8_t *buf = (uint8_t *) &big_endian_64_bits; + uint64_t ret = 0; + for (size_t i = 8; i; i--) { + ret |= ((uint64_t) buf[i - 1]) << ((8 - i) * 8); + } + return ret; +#endif +} +#endif + +/// Read given number of bytes into given buffer, display error if needed +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[out] buffer Where to save the results. +/// @param[in] length How many bytes should be read. +/// +/// @return kSDReadStatusSuccess if everything was OK, kSDReadStatusNotShaDa if +/// there were not enough bytes to read or kSDReadStatusReadError if +/// there was some error while reading. +static ShaDaReadResult fread_len(ShaDaReadDef *const sd_reader, + char *const buffer, + const size_t length) + 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) { + emsg2(_(SERR "System error while reading ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else if (sd_reader->eof) { + emsgu(_(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; +} + +/// Read next unsigned integer from file +/// +/// Errors out if the result is not an unsigned integer. +/// +/// Unlike msgpack own function this one works with `FILE *` and reads *exactly* +/// as much bytes as needed, making it possible to avoid both maintaining own +/// buffer and calling `fseek`. +/// +/// One byte from file stream is always consumed, even if it is not correct. +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[out] result Location where result is saved. +/// +/// @return kSDReadStatusSuccess if reading was successfull, +/// kSDReadStatusNotShaDa if there were not enough bytes to read or +/// kSDReadStatusReadError if reading failed for whatever reason. +static ShaDaReadResult msgpack_read_uint64(ShaDaReadDef *const sd_reader, + const int first_char, + uint64_t *const result) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const uintmax_t fpos = sd_reader->fpos - 1; + + if (first_char == EOF) { + if (sd_reader->error) { + emsg2(_(SERR "System error while reading integer from ShaDa file: %s"), + sd_reader->error); + return kSDReadStatusReadError; + } else if (sd_reader->eof) { + emsgu(_(RCERR "Error while reading ShaDa file: " + "expected positive integer at position %" PRIu64 + ", but got nothing"), + (uint64_t) fpos); + return kSDReadStatusNotShaDa; + } + } + + if (~first_char & 0x80) { + // Positive fixnum + *result = (uint64_t) ((uint8_t) first_char); + } else { + size_t length = 0; + switch (first_char) { + case 0xCC: { // uint8 + length = 1; + break; + } + case 0xCD: { // uint16 + length = 2; + break; + } + case 0xCE: { // uint32 + length = 4; + break; + } + case 0xCF: { // uint64 + length = 8; + break; + } + default: { + emsgu(_(RCERR "Error while reading ShaDa file: " + "expected positive integer at position %" PRIu64), + (uint64_t) fpos); + return kSDReadStatusNotShaDa; + } + } + uint64_t buf = 0; + char *buf_u8 = (char *) &buf; + ShaDaReadResult fl_ret; + if ((fl_ret = fread_len(sd_reader, &(buf_u8[sizeof(buf)-length]), length)) + != kSDReadStatusSuccess) { + return fl_ret; + } + *result = be64toh(buf); + } + return kSDReadStatusSuccess; +} + +/// Convert or copy and return a string +/// +/// @param[in] sd_conv Conversion definition. +/// @param[in] str String to convert. +/// @param[in] len String length. +/// +/// @return [allocated] converted string or copy of the original string. +static inline char *get_converted_string(const vimconv_T *const sd_conv, + const char *const str, + const size_t len) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_MALLOC FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (!has_non_ascii_len(str, len)) { + return xmemdupz(str, len); + } + size_t new_len = len; + char *const new_str = string_convert(sd_conv, str, &new_len); + if (new_str == NULL) { + return xmemdupz(str, len); + } + return new_str; +} + +#define READERR(entry_name, error_desc) \ + RERR "Error while reading ShaDa file: " \ + entry_name " entry at position %" PRIu64 " " \ + error_desc +#define CHECK_KEY(key, expected) ( \ + key.via.str.size == sizeof(expected) - 1 \ + && STRNCMP(key.via.str.ptr, expected, sizeof(expected) - 1) == 0) +#define CLEAR_GA_AND_ERROR_OUT(ga) \ + do { \ + ga_clear(&ga); \ + goto shada_read_next_item_error; \ + } while (0) +#define ID(s) s +#define BINDUP(b) xmemdupz(b.ptr, b.size) +#define TOINT(s) ((int) (s)) +#define TOLONG(s) ((long) (s)) +#define TOCHAR(s) ((char) (s)) +#define TOU8(s) ((uint8_t) (s)) +#define TOSIZE(s) ((size_t) (s)) +#define CHECKED_ENTRY(condition, error_desc, entry_name, obj, tgt, attr, \ + proc) \ + do { \ + if (!(condition)) { \ + emsgu(_(READERR(entry_name, error_desc)), initial_fpos); \ + CLEAR_GA_AND_ERROR_OUT(ad_ga); \ + } \ + tgt = proc(obj.via.attr); \ + } while (0) +#define CHECK_KEY_IS_STR(entry_name) \ + do { \ + if (unpacked.data.via.map.ptr[i].key.type != MSGPACK_OBJECT_STR) { \ + emsgu(_(READERR(entry_name, "has key which is not a string")), \ + initial_fpos); \ + CLEAR_GA_AND_ERROR_OUT(ad_ga); \ + } else if (unpacked.data.via.map.ptr[i].key.via.str.size == 0) { \ + emsgu(_(READERR(entry_name, "has empty key")), initial_fpos); \ + CLEAR_GA_AND_ERROR_OUT(ad_ga); \ + } \ + } while (0) +#define CHECKED_KEY(entry_name, name, error_desc, tgt, condition, attr, proc) \ + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, name)) { \ + CHECKED_ENTRY( \ + condition, "has " name " key value " error_desc, \ + entry_name, unpacked.data.via.map.ptr[i].val, \ + tgt, attr, proc); \ + } +#define TYPED_KEY(entry_name, name, type_name, tgt, objtype, attr, proc) \ + CHECKED_KEY( \ + entry_name, name, "which is not " type_name, tgt, \ + unpacked.data.via.map.ptr[i].val.type == MSGPACK_OBJECT_##objtype, \ + attr, proc) +#define BOOLEAN_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a boolean", tgt, BOOLEAN, boolean, ID) +#define STRING_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a binary", tgt, BIN, bin, BINDUP) +#define CONVERTED_STRING_KEY(entry_name, name, tgt) \ + TYPED_KEY(entry_name, name, "a binary", tgt, BIN, bin, BIN_CONVERTED) +#define INT_KEY(entry_name, name, tgt, proc) \ + CHECKED_KEY( \ + entry_name, name, "which is not an integer", tgt, \ + (unpacked.data.via.map.ptr[i].val.type \ + == MSGPACK_OBJECT_POSITIVE_INTEGER \ + || unpacked.data.via.map.ptr[i].val.type \ + == MSGPACK_OBJECT_NEGATIVE_INTEGER), \ + i64, proc) +#define INTEGER_KEY(entry_name, name, tgt) \ + INT_KEY(entry_name, name, tgt, TOINT) +#define LONG_KEY(entry_name, name, tgt) \ + INT_KEY(entry_name, name, tgt, TOLONG) +#define ADDITIONAL_KEY \ + { \ + ga_grow(&ad_ga, 1); \ + memcpy(((char *)ad_ga.ga_data) + ((size_t) ad_ga.ga_len \ + * sizeof(*unpacked.data.via.map.ptr)), \ + unpacked.data.via.map.ptr + i, \ + sizeof(*unpacked.data.via.map.ptr)); \ + ad_ga.ga_len++; \ + } +#define CONVERTED(str, len) ( \ + sd_reader->sd_conv.vc_type != CONV_NONE \ + ? get_converted_string(&sd_reader->sd_conv, (str), (len)) \ + : xmemdupz((str), (len))) +#define BIN_CONVERTED(b) CONVERTED(b.ptr, b.size) +#define SET_ADDITIONAL_DATA(tgt, name) \ + do { \ + if (ad_ga.ga_len) { \ + msgpack_object obj = { \ + .type = MSGPACK_OBJECT_MAP, \ + .via = { \ + .map = { \ + .size = (uint32_t) ad_ga.ga_len, \ + .ptr = ad_ga.ga_data, \ + } \ + } \ + }; \ + typval_T adtv; \ + if (msgpack_to_vim(obj, &adtv) == FAIL \ + || adtv.v_type != VAR_DICT) { \ + emsgu(_(READERR(name, \ + "cannot be converted to a VimL dictionary")), \ + initial_fpos); \ + ga_clear(&ad_ga); \ + clear_tv(&adtv); \ + goto shada_read_next_item_error; \ + } \ + tgt = adtv.vval.v_dict; \ + } \ + ga_clear(&ad_ga); \ + } while (0) +#define SET_ADDITIONAL_ELEMENTS(src, src_maxsize, tgt, name) \ + do { \ + if ((src).size > (size_t) (src_maxsize)) { \ + msgpack_object obj = { \ + .type = MSGPACK_OBJECT_ARRAY, \ + .via = { \ + .array = { \ + .size = ((src).size - (uint32_t) (src_maxsize)), \ + .ptr = (src).ptr + (src_maxsize), \ + } \ + } \ + }; \ + typval_T aetv; \ + if (msgpack_to_vim(obj, &aetv) == FAIL) { \ + emsgu(_(READERR(name, "cannot be converted to a VimL list")), \ + initial_fpos); \ + clear_tv(&aetv); \ + goto shada_read_next_item_error; \ + } \ + assert(aetv.v_type == VAR_LIST); \ + (tgt) = aetv.vval.v_list; \ + } \ + } while (0) + +/// Iterate over shada file contents +/// +/// @param[in] sd_reader Structure containing file reader definition. +/// @param[out] entry Address where next entry contents will be saved. +/// @param[in] flags Flags, determining whether and which items should be +/// skipped (see SRNIFlags enum). +/// @param[in] max_kbyte If non-zero, skip reading entries which have length +/// greater then given. +/// +/// @return Any value from ShaDaReadResult enum. +static ShaDaReadResult shada_read_next_item(ShaDaReadDef *const sd_reader, + ShadaEntry *const entry, + const unsigned flags, + const size_t max_kbyte) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + ShaDaReadResult ret = kSDReadStatusMalformed; +shada_read_next_item_start: + // Set entry type to kSDItemMissing and also make sure that all pointers in + // data union are NULL so they are safe to xfree(). This is needed in case + // somebody calls goto shada_read_next_item_error before anything is set in + // the switch. + memset(entry, 0, sizeof(*entry)); + if (sd_reader->eof) { + return kSDReadStatusFinished; + } + + // First: manually unpack type, timestamp and length. + // This is needed to avoid both seeking and having to maintain a buffer. + uint64_t type_u64 = (uint64_t) kSDItemMissing; + uint64_t timestamp_u64; + uint64_t length_u64; + + const uint64_t initial_fpos = (uint64_t) sd_reader->fpos; + const int first_char = read_char(sd_reader); + if (first_char == EOF && sd_reader->eof) { + return kSDReadStatusFinished; + } + + ShaDaReadResult mru_ret; + if (((mru_ret = msgpack_read_uint64(sd_reader, first_char, &type_u64)) + != kSDReadStatusSuccess) + || ((mru_ret = msgpack_read_uint64(sd_reader, read_char(sd_reader), + ×tamp_u64)) + != kSDReadStatusSuccess) + || ((mru_ret = msgpack_read_uint64(sd_reader, read_char(sd_reader), + &length_u64)) + != kSDReadStatusSuccess)) { + return mru_ret; + } + + const size_t length = (size_t) length_u64; + entry->timestamp = (Timestamp) timestamp_u64; + + if (type_u64 == 0) { + // kSDItemUnknown cannot possibly pass that far because it is -1 and that + // will fail in msgpack_read_uint64. But kSDItemMissing may and it will + // otherwise be skipped because (1 << 0) will never appear in flags. + emsgu(_(RCERR "Error while reading ShaDa file: " + "there is an item at position %" PRIu64 " " + "that must not be there: Missing items are " + "for internal uses only"), + initial_fpos); + return kSDReadStatusNotShaDa; + } + + if ((type_u64 > SHADA_LAST_ENTRY + ? !(flags & kSDReadUnknown) + : !((unsigned) (1 << type_u64) & flags)) + || (max_kbyte && length > max_kbyte * 1024)) { + // First entry is unknown or equal to "\n" (10)? Most likely this means that + // current file is not a ShaDa file because first item should normally be + // a header (excluding tests where first item is tested item). Check this by + // parsing entry contents: in non-ShaDa files this will most likely result + // in incomplete MessagePack string. + if (initial_fpos == 0 + && (type_u64 == '\n' || type_u64 > SHADA_LAST_ENTRY)) { + const ShaDaReadResult spm_ret = shada_parse_msgpack(sd_reader, length, + NULL, NULL); + if (spm_ret != kSDReadStatusSuccess) { + return spm_ret; + } + } else { + const ShaDaReadResult srs_ret = sd_reader_skip(sd_reader, length); + if (srs_ret != kSDReadStatusSuccess) { + return srs_ret; + } + } + goto shada_read_next_item_start; + } + + if (type_u64 > SHADA_LAST_ENTRY) { + entry->type = kSDItemUnknown; + entry->data.unknown_item.size = length; + entry->data.unknown_item.type = type_u64; + if (initial_fpos == 0) { + const ShaDaReadResult spm_ret = shada_parse_msgpack( + sd_reader, length, NULL, &entry->data.unknown_item.contents); + if (spm_ret != kSDReadStatusSuccess) { + entry->type = kSDItemMissing; + } + return spm_ret; + } else { + entry->data.unknown_item.contents = xmalloc(length); + const ShaDaReadResult fl_ret = fread_len( + sd_reader, entry->data.unknown_item.contents, length); + if (fl_ret != kSDReadStatusSuccess) { + shada_free_shada_entry(entry); + entry->type = kSDItemMissing; + } + return fl_ret; + } + } + + msgpack_unpacked unpacked; + char *buf = NULL; + + const ShaDaReadResult spm_ret = shada_parse_msgpack(sd_reader, length, + &unpacked, &buf); + if (spm_ret != kSDReadStatusSuccess) { + ret = spm_ret; + goto shada_read_next_item_error; + } + ret = kSDReadStatusMalformed; + entry->data = sd_default_values[type_u64].data; + switch ((ShadaEntryType) type_u64) { + case kSDItemHeader: { + if (!msgpack_rpc_to_dictionary(&(unpacked.data), &(entry->data.header))) { + emsgu(_(READERR("header", "is not a dictionary")), initial_fpos); + goto shada_read_next_item_error; + } + break; + } + case kSDItemSearchPattern: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(READERR("search pattern", "is not a dictionary")), + initial_fpos); + goto shada_read_next_item_error; + } + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("search pattern"); + BOOLEAN_KEY("search pattern", SEARCH_KEY_MAGIC, + entry->data.search_pattern.magic) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_SMARTCASE, + entry->data.search_pattern.smartcase) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_HAS_LINE_OFFSET, + entry->data.search_pattern.has_line_offset) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_PLACE_CURSOR_AT_END, + entry->data.search_pattern.place_cursor_at_end) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_IS_LAST_USED, + entry->data.search_pattern.is_last_used) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_IS_SUBSTITUTE_PATTERN, + entry->data.search_pattern.is_substitute_pattern) + else + BOOLEAN_KEY("search pattern", SEARCH_KEY_HIGHLIGHTED, + entry->data.search_pattern.highlighted) + else + INTEGER_KEY("search pattern", SEARCH_KEY_OFFSET, + entry->data.search_pattern.offset) + else + CONVERTED_STRING_KEY("search pattern", SEARCH_KEY_PAT, + entry->data.search_pattern.pat) + else + ADDITIONAL_KEY + } + if (entry->data.search_pattern.pat == NULL) { + emsgu(_(READERR("search pattern", "has no pattern")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA(entry->data.search_pattern.additional_data, + "search pattern"); + break; + } + case kSDItemChange: + case kSDItemJump: + case kSDItemGlobalMark: + case kSDItemLocalMark: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(READERR("mark", "is not a dictionary")), initial_fpos); + goto shada_read_next_item_error; + } + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("mark"); + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, KEY_NAME_CHAR)) { + if (type_u64 == kSDItemJump || type_u64 == kSDItemChange) { + emsgu(_(READERR("mark", "has n key which is only valid for " + "local and global mark entries")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + CHECKED_ENTRY( + (unpacked.data.via.map.ptr[i].val.type + == MSGPACK_OBJECT_POSITIVE_INTEGER), + "has n key value which is not an unsigned integer", + "mark", unpacked.data.via.map.ptr[i].val, + entry->data.filemark.name, u64, TOCHAR); + } else { + LONG_KEY("mark", KEY_LNUM, entry->data.filemark.mark.lnum) + else + INTEGER_KEY("mark", KEY_COL, entry->data.filemark.mark.col) + else + STRING_KEY("mark", KEY_FILE, entry->data.filemark.fname) + else + ADDITIONAL_KEY + } + } + if (entry->data.filemark.fname == NULL) { + emsgu(_(READERR("mark", "is missing file name")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.filemark.mark.lnum <= 0) { + emsgu(_(READERR("mark", "has invalid line number")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.filemark.mark.col < 0) { + emsgu(_(READERR("mark", "has invalid column number")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA(entry->data.filemark.additional_data, "mark"); + break; + } + case kSDItemRegister: { + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(READERR("register", "is not a dictionary")), initial_fpos); + goto shada_read_next_item_error; + } + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("register"); + TYPED_KEY("register", REG_KEY_TYPE, "an unsigned integer", + entry->data.reg.type, POSITIVE_INTEGER, u64, TOU8) + else + TYPED_KEY("register", KEY_NAME_CHAR, "an unsigned integer", + entry->data.reg.name, POSITIVE_INTEGER, u64, TOCHAR) + else + TYPED_KEY("register", REG_KEY_WIDTH, "an unsigned integer", + entry->data.reg.width, POSITIVE_INTEGER, u64, TOSIZE) + else + if (CHECK_KEY(unpacked.data.via.map.ptr[i].key, + REG_KEY_CONTENTS)) { + if (unpacked.data.via.map.ptr[i].val.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR( + "register", + "has " REG_KEY_CONTENTS " key with non-array value")), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (unpacked.data.via.map.ptr[i].val.via.array.size == 0) { + emsgu(_(READERR("register", + "has " REG_KEY_CONTENTS " key with empty array")), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + const msgpack_object_array arr = + unpacked.data.via.map.ptr[i].val.via.array; + for (size_t i = 0; i < arr.size; i++) { + if (arr.ptr[i].type != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("register", "has " REG_KEY_CONTENTS " array " + "with non-binary value")), initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + } + entry->data.reg.contents_size = arr.size; + entry->data.reg.contents = xmalloc(arr.size * sizeof(char *)); + for (size_t i = 0; i < arr.size; i++) { + entry->data.reg.contents[i] = BIN_CONVERTED(arr.ptr[i].via.bin); + } + } else { + ADDITIONAL_KEY + } + } + if (entry->data.reg.contents == NULL) { + emsgu(_(READERR("register", "has missing " REG_KEY_CONTENTS " array")), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA(entry->data.reg.additional_data, "register"); + break; + } + case kSDItemHistoryEntry: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("history", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size < 2) { + emsgu(_(READERR("history", "does not have enough elements")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type + != MSGPACK_OBJECT_POSITIVE_INTEGER) { + emsgu(_(READERR("history", "has wrong history type type")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type + != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("history", "has wrong history string type")), + initial_fpos); + goto shada_read_next_item_error; + } + if (memchr(unpacked.data.via.array.ptr[1].via.bin.ptr, 0, + unpacked.data.via.array.ptr[1].via.bin.size) != NULL) { + emsgu(_(READERR("history", "contains string with zero byte inside")), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.history_item.histtype = + (uint8_t) unpacked.data.via.array.ptr[0].via.u64; + const bool is_hist_search = + entry->data.history_item.histtype == HIST_SEARCH; + if (is_hist_search) { + if (unpacked.data.via.array.size < 3) { + emsgu(_(READERR("search history", + "does not have separator character")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[2].type + != MSGPACK_OBJECT_POSITIVE_INTEGER) { + emsgu(_(READERR("search history", + "has wrong history separator type")), initial_fpos); + goto shada_read_next_item_error; + } + entry->data.history_item.sep = + (char) unpacked.data.via.array.ptr[2].via.u64; + } + size_t strsize; + if (sd_reader->sd_conv.vc_type == CONV_NONE + || !has_non_ascii_len(unpacked.data.via.array.ptr[1].via.bin.ptr, + unpacked.data.via.array.ptr[1].via.bin.size)) { +shada_read_next_item_hist_no_conv: + strsize = ( + unpacked.data.via.array.ptr[1].via.bin.size + + 1 // Zero byte + + 1); // Separator character + entry->data.history_item.string = xmalloc(strsize); + memcpy(entry->data.history_item.string, + unpacked.data.via.array.ptr[1].via.bin.ptr, + unpacked.data.via.array.ptr[1].via.bin.size); + } else { + size_t len = unpacked.data.via.array.ptr[1].via.bin.size; + char *const converted = string_convert( + &sd_reader->sd_conv, unpacked.data.via.array.ptr[1].via.bin.ptr, + &len); + if (converted != NULL) { + strsize = len + 2; + entry->data.history_item.string = xrealloc(converted, strsize); + } else { + goto shada_read_next_item_hist_no_conv; + } + } + entry->data.history_item.string[strsize - 2] = 0; + entry->data.history_item.string[strsize - 1] = + entry->data.history_item.sep; + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, (2 + is_hist_search), + entry->data.history_item.additional_elements, + "history"); + break; + } + case kSDItemVariable: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("variable", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size < 2) { + emsgu(_(READERR("variable", "does not have enough elements")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("variable", "has wrong variable name type")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_NIL + || unpacked.data.via.array.ptr[1].type == MSGPACK_OBJECT_EXT) { + emsgu(_(READERR("variable", "has wrong variable value type")), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.global_var.name = + xmemdupz(unpacked.data.via.array.ptr[0].via.bin.ptr, + unpacked.data.via.array.ptr[0].via.bin.size); + if (msgpack_to_vim(unpacked.data.via.array.ptr[1], + &(entry->data.global_var.value)) == FAIL) { + emsgu(_(READERR("variable", "has value that cannot " + "be converted to the VimL value")), initial_fpos); + goto shada_read_next_item_error; + } + if (sd_reader->sd_conv.vc_type != CONV_NONE) { + typval_T tgttv; + var_item_copy(&sd_reader->sd_conv, + &entry->data.global_var.value, + &tgttv, + true, + 0); + clear_tv(&entry->data.global_var.value); + entry->data.global_var.value = tgttv; + } + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, 2, + entry->data.global_var.additional_elements, + "variable"); + break; + } + case kSDItemSubString: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("sub string", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size < 1) { + emsgu(_(READERR("sub string", "does not have enough elements")), + initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.ptr[0].type != MSGPACK_OBJECT_BIN) { + emsgu(_(READERR("sub string", "has wrong sub string type")), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.sub_string.sub = + BIN_CONVERTED(unpacked.data.via.array.ptr[0].via.bin); + SET_ADDITIONAL_ELEMENTS(unpacked.data.via.array, 1, + entry->data.sub_string.additional_elements, + "sub string"); + break; + } + case kSDItemBufferList: { + if (unpacked.data.type != MSGPACK_OBJECT_ARRAY) { + emsgu(_(READERR("buffer list", "is not an array")), initial_fpos); + goto shada_read_next_item_error; + } + if (unpacked.data.via.array.size == 0) { + break; + } + entry->data.buffer_list.buffers = + xcalloc(unpacked.data.via.array.size, + sizeof(*entry->data.buffer_list.buffers)); + for (size_t i = 0; i < unpacked.data.via.array.size; i++) { + entry->data.buffer_list.size++; + msgpack_unpacked unpacked_2 = (msgpack_unpacked) { + .data = unpacked.data.via.array.ptr[i], + }; + { + msgpack_unpacked unpacked = unpacked_2; + if (unpacked.data.type != MSGPACK_OBJECT_MAP) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry that is not a dictionary"), + initial_fpos); + goto shada_read_next_item_error; + } + entry->data.buffer_list.buffers[i].pos = default_pos; + garray_T ad_ga; + ga_init(&ad_ga, sizeof(*(unpacked.data.via.map.ptr)), 1); + { + const size_t j = i; + { + for (size_t i = 0; i < unpacked.data.via.map.size; i++) { + CHECK_KEY_IS_STR("buffer list entry"); + LONG_KEY("buffer list entry", KEY_LNUM, + entry->data.buffer_list.buffers[j].pos.lnum) + else + INTEGER_KEY("buffer list entry", KEY_COL, + entry->data.buffer_list.buffers[j].pos.col) + else + STRING_KEY("buffer list entry", KEY_FILE, + entry->data.buffer_list.buffers[j].fname) + else + ADDITIONAL_KEY + } + } + } + if (entry->data.buffer_list.buffers[i].pos.lnum <= 0) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry with invalid line number"), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.buffer_list.buffers[i].pos.col < 0) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry with invalid column number"), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + if (entry->data.buffer_list.buffers[i].fname == NULL) { + emsgu(_(RERR "Error while reading ShaDa file: " + "buffer list at position %" PRIu64 " " + "contains entry that does not have a file name"), + initial_fpos); + CLEAR_GA_AND_ERROR_OUT(ad_ga); + } + SET_ADDITIONAL_DATA( + entry->data.buffer_list.buffers[i].additional_data, + "buffer list entry"); + } + } + break; + } + case kSDItemMissing: + case kSDItemUnknown: { + assert(false); + } + } + entry->type = (ShadaEntryType) type_u64; + ret = kSDReadStatusSuccess; +shada_read_next_item_end: + if (buf != NULL) { + msgpack_unpacked_destroy(&unpacked); + xfree(buf); + } + return ret; +shada_read_next_item_error: + entry->type = (ShadaEntryType) type_u64; + shada_free_shada_entry(entry); + entry->type = kSDItemMissing; + goto shada_read_next_item_end; +} +#undef BIN_CONVERTED +#undef CONVERTED +#undef CHECK_KEY +#undef BOOLEAN_KEY +#undef CONVERTED_STRING_KEY +#undef STRING_KEY +#undef ADDITIONAL_KEY +#undef ID +#undef BINDUP +#undef TOCHAR +#undef TOINT +#undef TOLONG +#undef TYPED_KEY +#undef INT_KEY +#undef INTEGER_KEY +#undef LONG_KEY +#undef TOU8 +#undef TOSIZE +#undef SET_ADDITIONAL_DATA +#undef SET_ADDITIONAL_ELEMENTS +#undef CLEAR_GA_AND_ERROR_OUT + +/// Check whether "name" is on removable media (according to 'shada') +/// +/// @param[in] name Checked name. +/// +/// @return True if it is, false otherwise. +static bool shada_removable(const char *name) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + char *p; + char part[MAXPATHL + 1]; + bool retval = false; + + char *new_name = home_replace_save(NULL, name); + for (p = (char *) p_shada; *p; ) { + (void) copy_option_part(&p, part, ARRAY_SIZE(part), ", "); + if (part[0] == 'r') { + home_replace(NULL, part + 1, NameBuff, MAXPATHL, true); + size_t n = STRLEN(NameBuff); + if (mb_strnicmp(NameBuff, new_name, n) == 0) { + retval = true; + break; + } + } + } + xfree(new_name); + return retval; +} |