diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index a8f75066e8..b458679560 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -7543,8 +7543,10 @@ stdpath({what}) *stdpath()* *E6100* is stored here. data_dirs List Other data directories. log String Logs directory (for use by plugins too). + run String Run directory: temporary, local storage + for sockets, named pipes, etc. state String Session state directory: storage for file - drafts, undo, shada, named pipes, ... + drafts, undo, shada, etc. Example: > :echo stdpath("config") diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index 4a88939715..108a47c522 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -1328,8 +1328,9 @@ paths. *base-directories* *xdg* The "base" (root) directories conform to the XDG Base Directory Specification. https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html -The $XDG_CONFIG_HOME, $XDG_DATA_HOME and $XDG_STATE_HOME environment variables -are used if they exist, otherwise default values (listed below) are used. +The $XDG_CONFIG_HOME, $XDG_DATA_HOME, $XDG_RUNTIME_DIR, and $XDG_STATE_HOME +environment variables are used if defined, else default values (listed below) +are used. CONFIG DIRECTORY (DEFAULT) ~ *$XDG_CONFIG_HOME* Nvim: stdpath("config") @@ -1341,6 +1342,11 @@ DATA DIRECTORY (DEFAULT) ~ Unix: ~/.local/share ~/.local/share/nvim Windows: ~/AppData/Local ~/AppData/Local/nvim-data +RUN DIRECTORY (DEFAULT) ~ + *$XDG_RUNTIME_DIR* Nvim: stdpath("run") + Unix: /tmp/nvim.user/xxx /tmp/nvim.user/xxx + Windows: $TMP/nvim.user/xxx $TMP/nvim.user/xxx + STATE DIRECTORY (DEFAULT) ~ *$XDG_STATE_HOME* Nvim: stdpath("state") Unix: ~/.local/state ~/.local/state/nvim diff --git a/src/nvim/diff.c b/src/nvim/diff.c index f22933ec3c..e4d77cec9c 100644 --- a/src/nvim/diff.c +++ b/src/nvim/diff.c @@ -1206,7 +1206,7 @@ void ex_diffpatch(exarg_T *eap) || (os_chdir((char *)dirbuf) != 0)) { dirbuf[0] = NUL; } else { - char *tempdir = (char *)vim_gettempdir(); + char *tempdir = vim_gettempdir(); if (tempdir == NULL) { tempdir = "/tmp"; } diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index a0881a85d2..85ab51da5d 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -9719,6 +9719,8 @@ static void f_stdpath(typval_T *argvars, typval_T *rettv, FunPtr fptr) rettv->vval.v_string = get_xdg_home(kXDGStateHome); } else if (strequal(p, "log")) { rettv->vval.v_string = get_xdg_home(kXDGStateHome); + } else if (strequal(p, "run")) { + rettv->vval.v_string = stdpaths_get_xdg_var(kXDGRuntimeDir); } else if (strequal(p, "config_dirs")) { get_xdg_var_list(kXDGConfigDirs, rettv); } else if (strequal(p, "data_dirs")) { diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index ba3e7a0150..33ae1dbad0 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -5281,45 +5281,80 @@ void forward_slash(char_u *fname) } #endif -/// Name of Vim's own temp dir. Ends in a slash. -static char_u *vim_tempdir = NULL; +/// Path to Nvim's own temp dir. Ends in a slash. +static char *vim_tempdir = NULL; -/// Create a directory for private use by this instance of Neovim. -/// This is done once, and the same directory is used for all temp files. +/// Creates a directory for private use by this instance of Nvim, trying each of +/// `TEMP_DIR_NAMES` until one succeeds. +/// +/// Only done once, the same directory is used for all temp files. /// This method avoids security problems because of symlink attacks et al. /// It's also a bit faster, because we only need to check for an existing /// file when creating the directory and not for each temp file. -static void vim_maketempdir(void) +static void vim_mktempdir(void) { - static const char *temp_dirs[] = TEMP_DIR_NAMES; - // Try the entries in `TEMP_DIR_NAMES` to create the temp directory. - char_u template[TEMP_FILE_PATH_MAXLEN]; - char_u path[TEMP_FILE_PATH_MAXLEN]; + static const char *temp_dirs[] = TEMP_DIR_NAMES; // Try each of these until one succeeds. + char tmp[TEMP_FILE_PATH_MAXLEN]; + char path[TEMP_FILE_PATH_MAXLEN]; + char user[40] = { 0 }; + + (void)os_get_username(user, sizeof(user)); // Make sure the umask doesn't remove the executable bit. // "repl" has been reported to use "0177". mode_t umask_save = umask(0077); for (size_t i = 0; i < ARRAY_SIZE(temp_dirs); i++) { - // Expand environment variables, leave room for "/nvimXXXXXX/999999999" - expand_env((char_u *)temp_dirs[i], template, TEMP_FILE_PATH_MAXLEN - 22); - if (!os_isdir(template)) { // directory doesn't exist + // Expand environment variables, leave room for "/tmp/nvim./XXXXXX/999999999". + expand_env((char_u *)temp_dirs[i], (char_u *)tmp, TEMP_FILE_PATH_MAXLEN - 64); + if (!os_isdir((char_u *)tmp)) { continue; } - add_pathsep((char *)template); - // Concatenate with temporary directory name pattern - STRCAT(template, "nvimXXXXXX"); + // "/tmp/" exists, now try to create "/tmp/nvim./". + add_pathsep(tmp); + xstrlcat(tmp, "nvim.", sizeof(tmp)); + xstrlcat(tmp, user, sizeof(tmp)); + (void)os_mkdir(tmp, 0700); // Always create, to avoid a race. + bool owned = os_file_owned(tmp); + bool isdir = os_isdir((char_u *)tmp); +#ifdef UNIX + int perm = os_getperm(tmp); // XDG_RUNTIME_DIR must be owned by the user, mode 0700. + bool valid = isdir && owned && 0700 == (perm & 0777); +#else + bool valid = isdir && owned; // TODO(justinmk): Windows ACL? +#endif + if (valid) { + add_pathsep(tmp); + } else { + if (!owned) { + ELOG("tempdir root not owned by current user (%s): %s", user, tmp); + } else if (!isdir) { + ELOG("tempdir root not a directory: %s", tmp); + } +#ifdef UNIX + if (0700 != (perm & 0777)) { + ELOG("tempdir root has invalid permissions (%o): %s", perm, tmp); + } +#endif + // If our "root" tempdir is invalid or fails, proceed without "/". + // Else user1 could break user2 by creating "/tmp/nvim.user2/". + tmp[strlen(tmp) - strlen(user)] = '\0'; + } - if (os_mkdtemp((const char *)template, (char *)path) != 0) { + // Now try to create "/tmp/nvim./XXXXXX". + xstrlcat(tmp, "XXXXXX", sizeof(tmp)); // mkdtemp "template", will be replaced with random alphanumeric chars. + int r = os_mkdtemp(tmp, path); + if (r != 0) { + WLOG("tempdir create failed: %s: %s", os_strerror(r), tmp); continue; } - if (vim_settempdir((char *)path)) { + if (vim_settempdir(path)) { // Successfully created and set temporary directory so stop trying. break; } else { // Couldn't set `vim_tempdir` to `path` so remove created directory. - os_rmdir((char *)path); + os_rmdir(path); } } (void)umask(umask_save); @@ -5415,26 +5450,27 @@ void vim_deltempdir(void) { if (vim_tempdir != NULL) { // remove the trailing path separator - path_tail((char *)vim_tempdir)[-1] = NUL; - delete_recursive((const char *)vim_tempdir); + path_tail(vim_tempdir)[-1] = NUL; + delete_recursive(vim_tempdir); XFREE_CLEAR(vim_tempdir); } } -/// @return the name of temp directory. This directory would be created on the first -/// call to this function. -char_u *vim_gettempdir(void) +/// Gets path to Nvim's own temp dir (ending with slash). +/// +/// Creates the directory on the first call. +char *vim_gettempdir(void) { if (vim_tempdir == NULL) { - vim_maketempdir(); + vim_mktempdir(); } return vim_tempdir; } -/// Set Neovim own temporary directory name to `tempdir`. This directory should -/// be already created. Expand this name to a full path and put it in -/// `vim_tempdir`. This avoids that using `:cd` would confuse us. +/// Sets Nvim's own temporary directory name to `tempdir`. This directory must +/// already exist. Expands the name to a full path and put it in `vim_tempdir`. +/// This avoids that using `:cd` would confuse us. /// /// @param tempdir must be no longer than MAXPATHL. /// @@ -5447,7 +5483,7 @@ static bool vim_settempdir(char *tempdir) } vim_FullName(tempdir, buf, MAXPATHL, false); add_pathsep(buf); - vim_tempdir = (char_u *)xstrdup(buf); + vim_tempdir = xstrdup(buf); xfree(buf); return true; } @@ -5456,14 +5492,14 @@ static bool vim_settempdir(char *tempdir) /// /// @note The temp file is NOT created. /// -/// @return pointer to the temp file name or NULL if Neovim can't create +/// @return pointer to the temp file name or NULL if Nvim can't create /// temporary directory for its own temporary files. char_u *vim_tempname(void) { // Temp filename counter. static uint64_t temp_count; - char_u *tempdir = vim_gettempdir(); + char *tempdir = vim_gettempdir(); if (!tempdir) { return NULL; } diff --git a/src/nvim/hardcopy.c b/src/nvim/hardcopy.c index 230d5c430e..b96220d547 100644 --- a/src/nvim/hardcopy.c +++ b/src/nvim/hardcopy.c @@ -2508,7 +2508,7 @@ bool mch_print_begin(prt_settings_T *psettings) */ prt_dsc_start(); prt_dsc_textline("Title", (char *)psettings->jobname); - if (os_get_user_name(buffer, 256) == FAIL) { + if (os_get_username(buffer, 256) == FAIL) { STRCPY(buffer, "Unknown"); } prt_dsc_textline("For", buffer); diff --git a/src/nvim/main.c b/src/nvim/main.c index 2b5a5a9033..a7e39b7655 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -2016,14 +2016,14 @@ static void source_startup_scripts(const mparm_T *const parmp) // do_user_initialization. #if defined(UNIX) // If vimrc file is not owned by user, set 'secure' mode. - if (!file_owned(VIMRC_FILE)) + if (!os_file_owned(VIMRC_FILE)) // NOLINT(readability/braces) #endif secure = p_secure; if (do_source(VIMRC_FILE, true, DOSO_VIMRC) == FAIL) { #if defined(UNIX) // if ".exrc" is not owned by user set 'secure' mode - if (!file_owned(EXRC_FILE)) { + if (!os_file_owned(EXRC_FILE)) { secure = p_secure; } else { secure = 0; @@ -2068,23 +2068,6 @@ static int execute_env(char *env) return FAIL; } -#ifdef UNIX -/// Checks if user owns file. -/// Use both uv_fs_stat() and uv_fs_lstat() through os_fileinfo() and -/// os_fileinfo_link() respectively for extra security. -static bool file_owned(const char *fname) -{ - assert(fname != NULL); - uid_t uid = getuid(); - FileInfo file_info; - bool file_owned = os_fileinfo(fname, &file_info) - && file_info.stat.st_uid == uid; - bool link_owned = os_fileinfo_link(fname, &file_info) - && file_info.stat.st_uid == uid; - return file_owned && link_owned; -} -#endif - /// Prints the following then exits: /// - An error message `errstr` /// - A string `str` if not null diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 9da5aa54c5..43fa7a0dd7 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -311,7 +311,7 @@ int ml_open(buf_T *buf) b0p->b0_dirty = buf->b_changed ? B0_DIRTY : 0; b0p->b0_flags = get_fileformat(buf) + 1; set_b0_fname(b0p, buf); - (void)os_get_user_name((char *)b0p->b0_uname, B0_UNAME_SIZE); + (void)os_get_username((char *)b0p->b0_uname, B0_UNAME_SIZE); b0p->b0_uname[B0_UNAME_SIZE - 1] = NUL; os_get_hostname((char *)b0p->b0_hname, B0_HNAME_SIZE); b0p->b0_hname[B0_HNAME_SIZE - 1] = NUL; @@ -669,7 +669,7 @@ static void set_b0_fname(ZERO_BL *b0p, buf_T *buf) B0_FNAME_SIZE_CRYPT, true); if (b0p->b0_fname[0] == '~') { // If there is no user name or it is too long, don't use "~/" - int retval = os_get_user_name(uname, B0_UNAME_SIZE); + int retval = os_get_username(uname, B0_UNAME_SIZE); size_t ulen = STRLEN(uname); size_t flen = STRLEN(b0p->b0_fname); if (retval == FAIL || ulen + flen > B0_FNAME_SIZE_CRYPT - 1) { diff --git a/src/nvim/msgpack_rpc/server.c b/src/nvim/msgpack_rpc/server.c index c9e707aa92..b252f0998e 100644 --- a/src/nvim/msgpack_rpc/server.c +++ b/src/nvim/msgpack_rpc/server.c @@ -89,7 +89,7 @@ void server_teardown(void) /// /// Named pipe format: /// - Windows: "\\.\pipe\.." -/// - Other: "~/.local/state/nvim/.." +/// - Other: "/tmp/nvim.user/xxx/.." char *server_address_new(const char *name) { static uint32_t count = 0; @@ -98,7 +98,7 @@ char *server_address_new(const char *name) int r = snprintf(fmt, sizeof(fmt), "\\\\.\\pipe\\%s.%" PRIu64 ".%" PRIu32, name ? name : "nvim", os_get_pid(), count++); #else - char *dir = get_xdg_home(kXDGStateHome); + char *dir = stdpaths_get_xdg_var(kXDGRuntimeDir); int r = snprintf(fmt, sizeof(fmt), "%s/%s.%" PRIu64 ".%" PRIu32, dir, name ? name : "nvim", os_get_pid(), count++); xfree(dir); diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index 3a213605dc..2a7f7a221f 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -663,7 +663,7 @@ void expand_env_esc(char_u *restrict srcp, char_u *restrict dst, int dstlen, boo // Get the user directory. If this fails the shell is used to expand // ~user, which is slower and may fail on old versions of /bin/sh. var = (*dst == NUL) ? NULL - : (char_u *)os_get_user_directory((char *)dst + 1); + : (char_u *)os_get_userdir((char *)dst + 1); mustfree = true; if (var == NULL) { expand_T xpc; diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 68da53c476..7c5e4f31d7 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -126,7 +126,7 @@ bool os_isrealdir(const char *name) } } -/// Check if the given path is a directory or not. +/// Check if the given path exists and is a directory. /// /// @return `true` if `name` is a directory. bool os_isdir(const char_u *name) @@ -791,6 +791,27 @@ int os_setperm(const char *const name, int perm) return (r == kLibuvSuccess ? OK : FAIL); } +#ifdef UNIX +/// Checks if the current user owns a file. +/// +/// Uses both uv_fs_stat() and uv_fs_lstat() via os_fileinfo() and +/// os_fileinfo_link() respectively for extra security. +bool os_file_owned(const char *fname) + FUNC_ATTR_NONNULL_ALL +{ + uid_t uid = getuid(); + FileInfo finfo; + bool file_owned = os_fileinfo(fname, &finfo) && finfo.stat.st_uid == uid; + bool link_owned = os_fileinfo_link(fname, &finfo) && finfo.stat.st_uid == uid; + return file_owned && link_owned; +} +#else +bool os_file_owned(const char *fname) +{ + return true; // TODO(justinmk): Windows. #8244 +} +#endif + /// Changes the owner and group of a file, like chown(2). /// /// @return 0 on success, or libuv error code on failure. diff --git a/src/nvim/os/stdpaths.c b/src/nvim/os/stdpaths.c index a382392bd3..5576e7ba07 100644 --- a/src/nvim/os/stdpaths.c +++ b/src/nvim/os/stdpaths.c @@ -4,6 +4,7 @@ #include #include "nvim/ascii.h" +#include "nvim/fileio.h" #include "nvim/memory.h" #include "nvim/os/os.h" #include "nvim/os/stdpaths_defs.h" @@ -26,7 +27,7 @@ static const char *const xdg_defaults_env_vars[] = { [kXDGDataHome] = "LOCALAPPDATA", [kXDGCacheHome] = "TEMP", [kXDGStateHome] = "LOCALAPPDATA", - [kXDGRuntimeDir] = NULL, + [kXDGRuntimeDir] = NULL, // Decided by vim_mktempdir(). [kXDGConfigDirs] = NULL, [kXDGDataDirs] = NULL, }; @@ -41,7 +42,7 @@ static const char *const xdg_defaults[] = { [kXDGDataHome] = "~\\AppData\\Local", [kXDGCacheHome] = "~\\AppData\\Local\\Temp", [kXDGStateHome] = "~\\AppData\\Local", - [kXDGRuntimeDir] = NULL, + [kXDGRuntimeDir] = NULL, // Decided by vim_mktempdir(). [kXDGConfigDirs] = NULL, [kXDGDataDirs] = NULL, #else @@ -49,7 +50,7 @@ static const char *const xdg_defaults[] = { [kXDGDataHome] = "~/.local/share", [kXDGCacheHome] = "~/.cache", [kXDGStateHome] = "~/.local/state", - [kXDGRuntimeDir] = NULL, + [kXDGRuntimeDir] = NULL, // Decided by vim_mktempdir(). [kXDGConfigDirs] = "/etc/xdg/", [kXDGDataDirs] = "/usr/local/share/:/usr/share/", #endif @@ -83,6 +84,11 @@ char *stdpaths_get_xdg_var(const XDGVarType idx) ret = xstrdup(env_val); } else if (fallback) { ret = expand_env_save((char *)fallback); + } else if (idx == kXDGRuntimeDir) { + // Special-case: stdpath('run') is defined at startup. + ret = vim_gettempdir(); + size_t len = strlen(ret); + ret = xstrndup(ret, len >= 2 ? len - 1 : 0); // Trim trailing slash. } return ret; diff --git a/src/nvim/os/users.c b/src/nvim/os/users.c index 9fe97dd5e4..bd34e917b2 100644 --- a/src/nvim/os/users.c +++ b/src/nvim/os/users.c @@ -112,9 +112,13 @@ int os_get_usernames(garray_T *users) return OK; } -// Insert user name in s[len]. -// Return OK if a name found. -int os_get_user_name(char *s, size_t len) +/// Gets the username that owns the current Nvim process. +/// +/// @param s[out] Username. +/// @param len Length of `s`. +/// +/// @return OK if a name found. +int os_get_username(char *s, size_t len) { #ifdef UNIX return os_get_uname((uv_uid_t)getuid(), s, len); @@ -124,9 +128,13 @@ int os_get_user_name(char *s, size_t len) #endif } -// Insert user name for "uid" in s[len]. -// Return OK if a name found. -// If the name is not found, write the uid into s[len] and return FAIL. +/// Gets the username associated with `uid`. +/// +/// @param uid User id. +/// @param s[out] Username, or `uid` on failure. +/// @param len Length of `s`. +/// +/// @return OK if a username was found, else FAIL. int os_get_uname(uv_uid_t uid, char *s, size_t len) { #if defined(HAVE_PWD_H) && defined(HAVE_GETPWUID) @@ -142,10 +150,10 @@ int os_get_uname(uv_uid_t uid, char *s, size_t len) return FAIL; // a number is not a name } -// Returns the user directory for the given username. -// The caller has to free() the returned string. -// If the username is not found, NULL is returned. -char *os_get_user_directory(const char *name) +/// Gets the user directory for the given username, or NULL on failure. +/// +/// Caller must free() the returned string. +char *os_get_userdir(const char *name) { #if defined(HAVE_GETPWNAM) && defined(HAVE_PWD_H) if (name == NULL || *name == NUL) { diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index cdcf08c348..00a4dd041d 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -170,7 +170,7 @@ describe('server -> client', function() if method == "notification" then eq('done!', eval('rpcrequest('..cid..', "nested")')) elseif method == "nested_done" then - ok(false, 'this should never have been sent') + ok(false, 'never sent', 'sent') end end diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua index bc025771d1..a4d22685e8 100644 --- a/test/functional/core/fileio_spec.lua +++ b/test/functional/core/fileio_spec.lua @@ -1,22 +1,28 @@ local helpers = require('test.functional.helpers')(after_each) +local assert_log = helpers.assert_log +local assert_nolog = helpers.assert_nolog local clear = helpers.clear local command = helpers.command local eq = helpers.eq +local ok = helpers.ok local feed = helpers.feed local funcs = helpers.funcs local nvim_prog = helpers.nvim_prog local request = helpers.request local retry = helpers.retry local rmdir = helpers.rmdir +local matches = helpers.matches local mkdir = helpers.mkdir local sleep = helpers.sleep local read_file = helpers.read_file +local tmpname = helpers.tmpname local trim = helpers.trim local currentdir = helpers.funcs.getcwd local iswin = helpers.iswin local assert_alive = helpers.assert_alive local expect_exit = helpers.expect_exit +local write_file = helpers.write_file describe('fileio', function() before_each(function() @@ -140,3 +146,69 @@ describe('fileio', function() end) end) +describe('tmpdir', function() + local tmproot_pat = [=[.*[/\\]nvim%.[^/\\]+]=] + local testlog = 'Xtest_tmpdir_log' + local faketmp + + before_each(function() + -- Fake /tmp dir so that we can mess it up. + faketmp = tmpname() + os.remove(faketmp) + mkdir(faketmp) + end) + + after_each(function() + expect_exit(command, ':qall!') + os.remove(testlog) + end) + + it('failure modes', function() + clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=faketmp, } }) + assert_nolog('tempdir is not a directory', testlog) + assert_nolog('tempdir has invalid permissions', testlog) + + -- Tempfiles typically look like: "…/nvim./xxx/0". + -- - "…/nvim./xxx/" is the per-process tmpdir, not shared with other Nvims. + -- - "…/nvim./" is the tmpdir root, shared by all Nvims (normally). + local tmproot = (funcs.tempname()):match(tmproot_pat) + ok(tmproot:len() > 4, 'tmproot like "nvim.foo"', tmproot) + + -- Test how Nvim handles invalid tmpdir root (by hostile users or accidents). + -- + -- "…/nvim./" is not a directory: + expect_exit(command, ':qall!') + rmdir(tmproot) + write_file(tmproot, '') -- Not a directory, vim_mktempdir() should skip it. + clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=faketmp, } }) + matches(tmproot_pat, funcs.stdpath('run')) -- Tickle vim_mktempdir(). + -- Assert that broken tmpdir root was handled. + retry(nil, 1000, function() + assert_log('tempdir root not a directory', testlog, 100) + end) + + -- "…/nvim./" has wrong permissions: + if iswin() then + return -- TODO(justinmk): need setfperm/getfperm on Windows. #8244 + end + os.remove(testlog) + os.remove(tmproot) + mkdir(tmproot) + funcs.setfperm(tmproot, 'rwxr--r--') -- Invalid permissions, vim_mktempdir() should skip it. + clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=faketmp, } }) + matches(tmproot_pat, funcs.stdpath('run')) -- Tickle vim_mktempdir(). + -- Assert that broken tmpdir root was handled. + retry(nil, 1000, function() + assert_log('tempdir root has invalid permissions', testlog, 100) + end) + end) + + it('too long', function() + local bigname = ('%s/%s'):format(faketmp, ('x'):rep(666)) + mkdir(bigname) + clear({ env={ NVIM_LOG_FILE=testlog, TMPDIR=bigname, } }) + matches(tmproot_pat, funcs.stdpath('run')) -- Tickle vim_mktempdir(). + local len = (funcs.tempname()):len() + ok(len > 4 and len < 256, '4 < len < 256', tostring(len)) + end) +end) diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 5ba80a3646..fc9ea72ff2 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -399,7 +399,8 @@ describe('startup', function() eq({'ordinary', 'FANCY', 'mittel', 'FANCY after', 'ordinary after'}, exec_lua [[ return _G.test_loadorder ]]) local rtp = meths.get_option'rtp' - ok(startswith(rtp, 'test/functional/fixtures/nvim,test/functional/fixtures/pack/*/start/*,test/functional/fixtures/start/*,test/functional/fixtures,test/functional/fixtures/middle,'), 'rtp='..rtp) + ok(startswith(rtp, 'test/functional/fixtures/nvim,test/functional/fixtures/pack/*/start/*,test/functional/fixtures/start/*,test/functional/fixtures,test/functional/fixtures/middle,'), + 'startswith(…)', 'rtp='..rtp) end) it("handles the correct order with opt packages and after/", function() diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua index 92873e9937..9244ca0974 100644 --- a/test/functional/options/defaults_spec.lua +++ b/test/functional/options/defaults_spec.lua @@ -9,6 +9,7 @@ local clear = helpers.clear local exc_exec = helpers.exc_exec local eval = helpers.eval local eq = helpers.eq +local ok = helpers.ok local funcs = helpers.funcs local insert = helpers.insert local iswin = helpers.iswin @@ -238,7 +239,7 @@ describe('startup defaults', function() end) end) -describe('XDG-based defaults', function() +describe('XDG defaults', function() -- Need separate describe() blocks to not run clear() twice. -- Do not put before_each() here for the same reasons. @@ -282,6 +283,7 @@ describe('XDG-based defaults', function() eq('.', meths.get_option('viewdir')) eq('.', meths.get_option('directory')) eq('.', meths.get_option('undodir')) + ok((funcs.tempname()):len() > 4) end) end) @@ -306,6 +308,7 @@ describe('XDG-based defaults', function() .. env_sep.. root_path .. ('/b'):rep(2048) .. (env_sep .. root_path .. '/c'):rep(512)), XDG_DATA_HOME=(root_path .. ('/X'):rep(4096)), + XDG_RUNTIME_DIR=(root_path .. ('/X'):rep(4096)), XDG_STATE_HOME=(root_path .. ('/X'):rep(4096)), XDG_DATA_DIRS=(root_path .. ('/A'):rep(2048) .. env_sep .. root_path .. ('/B'):rep(2048) @@ -376,6 +379,7 @@ describe('XDG-based defaults', function() XDG_CONFIG_HOME='$XDG_DATA_HOME', XDG_CONFIG_DIRS='$XDG_DATA_DIRS', XDG_DATA_HOME='$XDG_CONFIG_HOME', + XDG_RUNTIME_DIR='$XDG_RUNTIME_DIR', XDG_STATE_HOME='$XDG_CONFIG_HOME', XDG_DATA_DIRS='$XDG_CONFIG_DIRS', }}) @@ -438,6 +442,7 @@ describe('XDG-based defaults', function() meths.get_option('undodir'):gsub('\\', '/')) eq(('$XDG_CONFIG_HOME/' .. state_dir .. '/view//'), meths.get_option('viewdir'):gsub('\\', '/')) + eq(nil, (funcs.tempname()):match('XDG_RUNTIME_DIR')) end) end) @@ -519,6 +524,7 @@ describe('stdpath()', function() eq(statedir, funcs.fnamemodify(funcs.stdpath('state'), ':t')) eq('table', type(funcs.stdpath('config_dirs'))) eq('table', type(funcs.stdpath('data_dirs'))) + eq('string', type(funcs.stdpath('run'))) assert_alive() -- Check for crash. #8393 end) diff --git a/test/functional/terminal/api_spec.lua b/test/functional/terminal/api_spec.lua index b3a0b8a2c8..5305b8af9c 100644 --- a/test/functional/terminal/api_spec.lua +++ b/test/functional/terminal/api_spec.lua @@ -48,8 +48,8 @@ describe('api', function() {3:-- TERMINAL --} | ]]) - ok(socket_session1:request("nvim_ui_attach", 42, 6, {rgb=true})) - ok(socket_session2:request("nvim_ui_attach", 25, 30, {rgb=true})) + ok((socket_session1:request("nvim_ui_attach", 42, 6, {rgb=true}))) + ok((socket_session2:request("nvim_ui_attach", 25, 30, {rgb=true}))) socket_session1:notify("nvim_input", "\n[socket 1] this is more than 25 columns") socket_session2:notify("nvim_input", "\n[socket 2] input") diff --git a/test/functional/vimscript/server_spec.lua b/test/functional/vimscript/server_spec.lua index ea07be231d..6e95459630 100644 --- a/test/functional/vimscript/server_spec.lua +++ b/test/functional/vimscript/server_spec.lua @@ -5,6 +5,7 @@ local iswin = helpers.iswin local ok = helpers.ok local matches = helpers.matches local pcall_err = helpers.pcall_err +local mkdir = helpers.mkdir local function clear_serverlist() for _, server in pairs(funcs.serverlist()) do @@ -13,9 +14,19 @@ local function clear_serverlist() end describe('server', function() - before_each(clear) + it('serverstart() stores sockets in $XDG_RUNTIME_DIR', function() + local dir = 'Xtest_xdg_run' + mkdir(dir) + clear({ env={ XDG_RUNTIME_DIR=dir } }) + matches(dir, funcs.stdpath('run')) + if not iswin() then + matches(dir, funcs.serverstart()) + end + end) + it('serverstart(), serverstop() does not set $NVIM', function() + clear() local s = eval('serverstart()') assert(s ~= nil and s:len() > 0, "serverstart() returned empty") eq('', eval('$NVIM')) @@ -34,6 +45,7 @@ describe('server', function() end) it('sets v:servername at startup or if all servers were stopped', function() + clear() local initial_server = meths.get_vvar('servername') assert(initial_server ~= nil and initial_server:len() > 0, 'v:servername was not initialized') @@ -62,11 +74,13 @@ describe('server', function() end) it('serverstop() returns false for invalid input', function() + clear() eq(0, eval("serverstop('')")) eq(0, eval("serverstop('bogus-socket-name')")) end) it('parses endpoints', function() + clear() clear_serverlist() eq({}, funcs.serverlist()) @@ -111,6 +125,7 @@ describe('server', function() end) it('serverlist() returns the list of servers', function() + clear() -- There should already be at least one server. local n = eval('len(serverlist())') diff --git a/test/functional/vimscript/timer_spec.lua b/test/functional/vimscript/timer_spec.lua index 20f2afb20f..5463cfb234 100644 --- a/test/functional/vimscript/timer_spec.lua +++ b/test/functional/vimscript/timer_spec.lua @@ -96,7 +96,7 @@ describe('timers', function() nvim_async("command", "let g:val = 0 | let g:c = getchar()") retry(nil, nil, function() local val = eval("g:val") - ok(val >= 2, "expected >= 2, got: "..tostring(val)) + ok(val >= 2, '>= 2', tostring(val)) eq(0, eval("getchar(1)")) end) feed("c") diff --git a/test/helpers.lua b/test/helpers.lua index 9a77ca1956..7ec9beea92 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -57,8 +57,16 @@ end function module.neq(expected, actual, context) return assert.are_not.same(expected, actual, context) end -function module.ok(res, msg) - return assert.is_true(res, msg) + +--- Asserts that `cond` is true, or prints a message. +--- +--- @param cond (boolean) expression to assert +--- @param expected (any) description of expected result +--- @param actual (any) description of actual result +function module.ok(cond, expected, actual) + assert((not expected and not actual) or (expected and actual), 'if "expected" is given, "actual" is also required') + local msg = expected and ('expected %s, got: %s'):format(expected, tostring(actual)) or nil + return assert(cond, msg) end local function epicfail(state, arguments, _) @@ -77,20 +85,33 @@ function module.matches(pat, actual) error(string.format('Pattern does not match.\nPattern:\n%s\nActual:\n%s', pat, actual)) end ---- Asserts that `pat` matches one or more lines in the tail of $NVIM_LOG_FILE. +--- Asserts that `pat` matches (or *not* if inverse=true) any line in the tail of `logfile`. --- ----@param pat string Lua pattern to search for in the log file ----@param logfile string Full path to log file (default=$NVIM_LOG_FILE) ----@param nrlines number Search up to this many log lines -function module.assert_log(pat, logfile, nrlines) +---@param pat (string) Lua pattern to match lines in the log file +---@param logfile (string) Full path to log file (default=$NVIM_LOG_FILE) +---@param nrlines (number) Search up to this many log lines +---@param inverse (boolean) Assert that the pattern does NOT match. +function module.assert_log(pat, logfile, nrlines, inverse) logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog' + assert(logfile ~= nil, 'no logfile') nrlines = nrlines or 10 + inverse = inverse or false local lines = module.read_file_list(logfile, -nrlines) or {} + local msg = string.format('Pattern %q %sfound in log (last %d lines): %s:\n%s', + pat, (inverse and '' or 'not '), nrlines, logfile, ' '..table.concat(lines, '\n ')) for _,line in ipairs(lines) do - if line:match(pat) then return end + if line:match(pat) then + if inverse then error(msg) else return end + end end - error(string.format('Pattern %q not found in log (last %d lines): %s:\n%s', - pat, nrlines, logfile, ' '..table.concat(lines, '\n '))) + if not inverse then error(msg) end +end + +--- Asserts that `pat` does NOT matche any line in the tail of `logfile`. +--- +--- @see assert_log +function module.assert_nolog(pat, logfile, nrlines) + return module.assert_log(pat, logfile, nrlines, true) end -- Invokes `fn` and returns the error string (with truncated paths), or raises @@ -284,6 +305,7 @@ local function tmpdir_is_local(dir) return not not (dir and string.find(dir, 'Xtest')) end +--- Creates a new temporary file for use by tests. module.tmpname = (function() local seq = 0 local tmpdir = tmpdir_get() diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua index c039b95d16..71177f4c65 100644 --- a/test/unit/os/env_spec.lua +++ b/test/unit/os/env_spec.lua @@ -266,7 +266,7 @@ describe('env.c', function() itp('does not crash #3725', function() local name_out = ffi.new('char[100]') - cimp.os_get_user_name(name_out, 100) + cimp.os_get_username(name_out, 100) local curuser = ffi.string(name_out) local src = to_cstr("~"..curuser.."/Vcs/django-rest-framework/rest_framework/renderers.py") diff --git a/test/unit/os/users_spec.lua b/test/unit/os/users_spec.lua index f92413c7de..679e76fae1 100644 --- a/test/unit/os/users_spec.lua +++ b/test/unit/os/users_spec.lua @@ -48,10 +48,10 @@ describe('users function', function() end) end) - describe('os_get_user_name', function() + describe('os_get_username', function() itp('should write the username into the buffer and return OK', function() local name_out = ffi.new('char[100]') - eq(OK, users.os_get_user_name(name_out, 100)) + eq(OK, users.os_get_username(name_out, 100)) eq(current_username, ffi.string(name_out)) end) end) @@ -73,18 +73,18 @@ describe('users function', function() end) end) - describe('os_get_user_directory', function() + describe('os_get_userdir', function() itp('should return NULL if called with NULL', function() - eq(NULL, users.os_get_user_directory(NULL)) + eq(NULL, users.os_get_userdir(NULL)) end) itp('should return $HOME for the current user', function() local home = os.getenv('HOME') - eq(home, ffi.string((users.os_get_user_directory(current_username)))) + eq(home, ffi.string((users.os_get_userdir(current_username)))) end) itp('should return NULL if the user is not found', function() - eq(NULL, users.os_get_user_directory('neovim_user_not_found_test')) + eq(NULL, users.os_get_userdir('neovim_user_not_found_test')) end) end) end) diff --git a/test/unit/tempfile_spec.lua b/test/unit/tempfile_spec.lua index c05abfd640..44bd19c1d2 100644 --- a/test/unit/tempfile_spec.lua +++ b/test/unit/tempfile_spec.lua @@ -24,7 +24,7 @@ describe('tempfile related functions', function() end describe('vim_gettempdir', function() - itp('returns path to Neovim own temp directory', function() + itp('returns path to Nvim own temp directory', function() local dir = vim_gettempdir() assert.True(dir ~= nil and dir:len() > 0) -- os_file_is_writable returns 2 for a directory which we have rights @@ -36,9 +36,7 @@ describe('tempfile related functions', function() end) itp('returns the same directory on each call', function() - local dir1 = vim_gettempdir() - local dir2 = vim_gettempdir() - eq(dir1, dir2) + eq(vim_gettempdir(), vim_gettempdir()) end) end) @@ -54,12 +52,10 @@ describe('tempfile related functions', function() end) itp('generate different names on each call', function() - local fst = vim_tempname() - local snd = vim_tempname() - neq(fst, snd) + neq(vim_tempname(), vim_tempname()) end) - itp('generate file name in Neovim own temp directory', function() + itp('generate file name in Nvim own temp directory', function() local dir = vim_gettempdir() local file = vim_tempname() eq(string.sub(file, 1, string.len(dir)), dir)