mirror of
				https://github.com/neovim/neovim.git
				synced 2025-11-04 09:44:31 +00:00 
			
		
		
		
	fix(path): avoid chdir() when resolving path (#28799)
Use uv_fs_realpath() instead.
It seems that uv_fs_realpath() has some problems on non-Linux platforms:
- macOS and other BSDs: this function will fail with UV_ELOOP if more
  than 32 symlinks are found while resolving the given path.  This limit
  is hardcoded and cannot be sidestepped.
- Windows: while this function works in the common case, there are a
  number of corner cases where it doesn't:
  - Paths in ramdisk volumes created by tools which sidestep the Volume
    Manager (such as ImDisk) cannot be resolved.
  - Inconsistent casing when using drive letters.
  - Resolved path bypasses subst'd drives.
Ref: https://docs.libuv.org/en/v1.x/fs.html#c.uv_fs_realpath
I don't know if the old implementation that uses uv_chdir() and uv_cwd()
also suffers from the same problems.
- For the ELOOP case, chdir() seems to have the same limitations.
- On Windows, Vim doesn't use anything like chdir() either. It uses
  _wfullpath(), while libuv uses GetFinalPathNameByHandleW().
			
			
This commit is contained in:
		@@ -6479,7 +6479,7 @@ static void f_resolve(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
 | 
			
		||||
  char *v = os_resolve_shortcut(fname);
 | 
			
		||||
  if (v == NULL) {
 | 
			
		||||
    if (os_is_reparse_point_include(fname)) {
 | 
			
		||||
      v = os_realpath(fname, v);
 | 
			
		||||
      v = os_realpath(fname, NULL, MAXPATHL + 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  rettv->vval.v_string = (v == NULL ? xstrdup(fname) : v);
 | 
			
		||||
@@ -6631,7 +6631,7 @@ static void f_resolve(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
 | 
			
		||||
    xfree(buf);
 | 
			
		||||
  }
 | 
			
		||||
# else
 | 
			
		||||
  char *v = os_realpath(fname, NULL);
 | 
			
		||||
  char *v = os_realpath(fname, NULL, MAXPATHL + 1);
 | 
			
		||||
  rettv->vval.v_string = v == NULL ? xstrdup(fname) : v;
 | 
			
		||||
# endif
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -474,18 +474,10 @@ void init_homedir(void)
 | 
			
		||||
    var = os_homedir();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (var != NULL) {
 | 
			
		||||
    // Change to the directory and get the actual path.  This resolves
 | 
			
		||||
    // links.  Don't do it when we can't return.
 | 
			
		||||
    if (os_dirname(os_buf, MAXPATHL) == OK && os_chdir(os_buf) == 0) {
 | 
			
		||||
      if (!os_chdir(var) && os_dirname(IObuff, IOSIZE) == OK) {
 | 
			
		||||
  // Get the actual path.  This resolves links.
 | 
			
		||||
  if (var != NULL && os_realpath(var, IObuff, IOSIZE) != NULL) {
 | 
			
		||||
    var = IObuff;
 | 
			
		||||
  }
 | 
			
		||||
      if (os_chdir(os_buf) != 0) {
 | 
			
		||||
        emsg(_(e_prev_dir));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fall back to current working directory if home is not found
 | 
			
		||||
  if ((var == NULL || *var == NUL)
 | 
			
		||||
 
 | 
			
		||||
@@ -1320,22 +1320,22 @@ bool os_fileid_equal_fileinfo(const FileID *file_id, const FileInfo *file_info)
 | 
			
		||||
/// Return the canonicalized absolute pathname.
 | 
			
		||||
///
 | 
			
		||||
/// @param[in] name Filename to be canonicalized.
 | 
			
		||||
/// @param[out] buf Buffer to store the canonicalized values. A minimum length
 | 
			
		||||
//                  of MAXPATHL+1 is required. If it is NULL, memory is
 | 
			
		||||
//                  allocated. In that case, the caller should deallocate this
 | 
			
		||||
//                  buffer.
 | 
			
		||||
/// @param[out] buf Buffer to store the canonicalized values.
 | 
			
		||||
///                 If it is NULL, memory is allocated. In that case, the caller
 | 
			
		||||
///                 should deallocate this buffer.
 | 
			
		||||
/// @param[in] len  The length of the buffer.
 | 
			
		||||
///
 | 
			
		||||
/// @return pointer to the buf on success, or NULL.
 | 
			
		||||
char *os_realpath(const char *name, char *buf)
 | 
			
		||||
char *os_realpath(const char *name, char *buf, size_t len)
 | 
			
		||||
  FUNC_ATTR_NONNULL_ARG(1)
 | 
			
		||||
{
 | 
			
		||||
  uv_fs_t request;
 | 
			
		||||
  int result = uv_fs_realpath(NULL, &request, name, NULL);
 | 
			
		||||
  if (result == kLibuvSuccess) {
 | 
			
		||||
    if (buf == NULL) {
 | 
			
		||||
      buf = xmallocz(MAXPATHL);
 | 
			
		||||
      buf = xmalloc(len);
 | 
			
		||||
    }
 | 
			
		||||
    xstrlcpy(buf, request.ptr, MAXPATHL + 1);
 | 
			
		||||
    xstrlcpy(buf, request.ptr, len);
 | 
			
		||||
  }
 | 
			
		||||
  uv_fs_req_cleanup(&request);
 | 
			
		||||
  return result == kLibuvSuccess ? buf : NULL;
 | 
			
		||||
 
 | 
			
		||||
@@ -2273,13 +2273,20 @@ bool match_suffix(char *fname)
 | 
			
		||||
/// @return `FAIL` for failure, `OK` for success.
 | 
			
		||||
int path_full_dir_name(char *directory, char *buffer, size_t len)
 | 
			
		||||
{
 | 
			
		||||
  int SUCCESS = 0;
 | 
			
		||||
  int retval = OK;
 | 
			
		||||
 | 
			
		||||
  if (strlen(directory) == 0) {
 | 
			
		||||
    return os_dirname(buffer, len);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (os_realpath(directory, buffer, len) != NULL) {
 | 
			
		||||
    return OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Path does not exist (yet).  For a full path fail, will use the path as-is.
 | 
			
		||||
  if (path_is_absolute(directory)) {
 | 
			
		||||
    return FAIL;
 | 
			
		||||
  }
 | 
			
		||||
  // For a relative path use the current directory and append the file name.
 | 
			
		||||
 | 
			
		||||
  char old_dir[MAXPATHL];
 | 
			
		||||
 | 
			
		||||
  // Get current directory name.
 | 
			
		||||
@@ -2287,36 +2294,12 @@ int path_full_dir_name(char *directory, char *buffer, size_t len)
 | 
			
		||||
    return FAIL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We have to get back to the current dir at the end, check if that works.
 | 
			
		||||
  if (os_chdir(old_dir) != SUCCESS) {
 | 
			
		||||
  xstrlcpy(buffer, old_dir, len);
 | 
			
		||||
  if (append_path(buffer, directory, len) == FAIL) {
 | 
			
		||||
    return FAIL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (os_chdir(directory) != SUCCESS) {
 | 
			
		||||
    // Path does not exist (yet).  For a full path fail,
 | 
			
		||||
    // will use the path as-is.  For a relative path use
 | 
			
		||||
    // the current directory and append the file name.
 | 
			
		||||
    if (path_is_absolute(directory)) {
 | 
			
		||||
      // Do not return immediately since we may be in the wrong directory.
 | 
			
		||||
      retval = FAIL;
 | 
			
		||||
    } else {
 | 
			
		||||
      xstrlcpy(buffer, old_dir, len);
 | 
			
		||||
      if (append_path(buffer, directory, len) == FAIL) {
 | 
			
		||||
        retval = FAIL;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else if (os_dirname(buffer, len) == FAIL) {
 | 
			
		||||
    // Do not return immediately since we are in the wrong directory.
 | 
			
		||||
    retval = FAIL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (os_chdir(old_dir) != SUCCESS) {
 | 
			
		||||
    // That shouldn't happen, since we've tested if it works.
 | 
			
		||||
    retval = FAIL;
 | 
			
		||||
    emsg(_(e_prev_dir));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return retval;
 | 
			
		||||
  return OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Append to_append to path with a slash in between.
 | 
			
		||||
 
 | 
			
		||||
@@ -22,11 +22,16 @@ local buffer = nil
 | 
			
		||||
 | 
			
		||||
describe('path.c', function()
 | 
			
		||||
  describe('path_full_dir_name', function()
 | 
			
		||||
    local old_dir
 | 
			
		||||
 | 
			
		||||
    setup(function()
 | 
			
		||||
      old_dir = uv.cwd()
 | 
			
		||||
      mkdir('unit-test-directory')
 | 
			
		||||
      uv.fs_symlink(old_dir .. '/unit-test-directory', 'unit-test-symlink')
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    teardown(function()
 | 
			
		||||
      uv.fs_unlink('unit-test-symlink')
 | 
			
		||||
      uv.fs_rmdir('unit-test-directory')
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
@@ -37,35 +42,64 @@ describe('path.c', function()
 | 
			
		||||
 | 
			
		||||
    before_each(function()
 | 
			
		||||
      -- Create empty string buffer which will contain the resulting path.
 | 
			
		||||
      length = string.len(uv.cwd()) + 22
 | 
			
		||||
      length = string.len(old_dir) + 22
 | 
			
		||||
      buffer = cstr(length, '')
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    after_each(function()
 | 
			
		||||
      uv.chdir(old_dir)
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    itp('returns the absolute directory name of a given relative one', function()
 | 
			
		||||
      local result = path_full_dir_name('..', buffer, length)
 | 
			
		||||
      eq(OK, result)
 | 
			
		||||
      local old_dir = uv.cwd()
 | 
			
		||||
      eq(OK, path_full_dir_name('..', buffer, length))
 | 
			
		||||
      uv.chdir('..')
 | 
			
		||||
      local expected = uv.cwd()
 | 
			
		||||
      uv.chdir(old_dir)
 | 
			
		||||
      eq(expected, (ffi.string(buffer)))
 | 
			
		||||
      eq(expected, ffi.string(buffer))
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    itp('returns the current directory name if the given string is empty', function()
 | 
			
		||||
      eq(OK, (path_full_dir_name('', buffer, length)))
 | 
			
		||||
      eq(uv.cwd(), (ffi.string(buffer)))
 | 
			
		||||
      eq(OK, path_full_dir_name('', buffer, length))
 | 
			
		||||
      eq(old_dir, ffi.string(buffer))
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    local function test_full_dir_absolute()
 | 
			
		||||
      itp('works with a normal absolute dir', function()
 | 
			
		||||
        eq(OK, path_full_dir_name(old_dir .. '/unit-test-directory', buffer, length))
 | 
			
		||||
        eq(old_dir .. '/unit-test-directory', ffi.string(buffer))
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      itp('works with a symlinked absolute dir', function()
 | 
			
		||||
        eq(OK, path_full_dir_name(old_dir .. '/unit-test-symlink', buffer, length))
 | 
			
		||||
        eq(old_dir .. '/unit-test-directory', ffi.string(buffer))
 | 
			
		||||
      end)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test_full_dir_absolute()
 | 
			
		||||
 | 
			
		||||
    describe('when cwd does not exist #28786', function()
 | 
			
		||||
      before_each(function()
 | 
			
		||||
        mkdir('dir-to-remove')
 | 
			
		||||
        uv.chdir('dir-to-remove')
 | 
			
		||||
        uv.fs_rmdir(old_dir .. '/dir-to-remove')
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      test_full_dir_absolute()
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    itp('works with a normal relative dir', function()
 | 
			
		||||
      local result = path_full_dir_name('unit-test-directory', buffer, length)
 | 
			
		||||
      eq(uv.cwd() .. '/unit-test-directory', (ffi.string(buffer)))
 | 
			
		||||
      eq(OK, result)
 | 
			
		||||
      eq(OK, path_full_dir_name('unit-test-directory', buffer, length))
 | 
			
		||||
      eq(old_dir .. '/unit-test-directory', ffi.string(buffer))
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    itp('works with a symlinked relative dir', function()
 | 
			
		||||
      eq(OK, path_full_dir_name('unit-test-symlink', buffer, length))
 | 
			
		||||
      eq(old_dir .. '/unit-test-directory', ffi.string(buffer))
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    itp('works with a non-existing relative dir', function()
 | 
			
		||||
      local result = path_full_dir_name('does-not-exist', buffer, length)
 | 
			
		||||
      eq(uv.cwd() .. '/does-not-exist', (ffi.string(buffer)))
 | 
			
		||||
      eq(OK, result)
 | 
			
		||||
      eq(OK, path_full_dir_name('does-not-exist', buffer, length))
 | 
			
		||||
      eq(old_dir .. '/does-not-exist', ffi.string(buffer))
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    itp('fails with a non-existing absolute dir', function()
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user