fix(cwd): validate getcwd(…, -1)

This commit is contained in:
Justin M. Keyes
2026-08-04 15:45:45 +02:00
parent db2e86fba4
commit 46ca236525
10 changed files with 99 additions and 73 deletions

View File

@@ -1357,7 +1357,7 @@ Use |getcwd()| to get the effective directory from the current scope. The
window-local working directory takes precedence over the buffer-local working
directory, which takes precedence over the tab-local working directory, which
in turn takes precedence over the global working directory. If a local
working directory (window, buffer or tab) does not exist, the next-higher
working directory (tab, window or buffer) does not exist, the next-higher
scope in the hierarchy applies.
*:cd* *E747* *E472*
@@ -1388,7 +1388,7 @@ scope in the hierarchy applies.
*:tc* *:tcd*
:tc[d][!] {path} Like |:cd|, but only set the directory for the current
tab. The current buffer and window will also use this
tab. The current window and buffer will also use this
directory. The current directory is not changed for
windows in other tabs and for windows in the current
tab that have their own window-local directory, or for
@@ -1401,15 +1401,10 @@ scope in the hierarchy applies.
*:tch* *:tchdir*
:tch[dir][!] Same as |:tcd|.
*:bc* *:bcd*
:bc[d][!] {path} Like |:cd|, but only set the current directory for the
current buffer. The current directory for other
windows or buffers or tabs is not changed.
*:lc* *:lcd*
:lc[d][!] {path} Like |:cd|, but only set the current directory for the
current window. The current directory for other
windows, tabs, or buffers is not changed.
tabs, windows, or buffers is not changed.
*:lch* *:lchdir*
:lch[dir][!] Same as |:lcd|.
@@ -1418,6 +1413,18 @@ scope in the hierarchy applies.
:lc[d][!] - Change to the previous current directory (before the
previous ":lcd {path}" command).
*:bc* *:bcd*
:bc[d][!] [path] Like |:cd|, but only set the current directory for the
current buffer. The current directory for other
tabs, windows, or buffers is not changed.
*:bcd-*
:bc[d][!] - Change to the previous buffer-local current directory
(before the last ":bcd [path]" command).
*:bch* *:bchdir*
:bch[dir][!] Same as |:bcd|.
*:pw* *:pwd* *E187*
:pw[d] Print the current directory name.
Also see |getcwd()|.
@@ -1429,24 +1436,34 @@ scope in the hierarchy applies.
:verbose pwd
[global] /path/to/current
" Set by :lcd
:verbose pwd
[window] /path/to/current
" Set by :tcd
:verbose pwd
[tabpage] /path/to/current
So long as no |:lcd| or |:tcd| command has been used, all windows share the
same current directory. Using a command to jump to another window doesn't
change anything for the current directory.
" Set by :lcd
:verbose pwd
[window] /path/to/current
" Set by :bcd
:verbose pwd
[buffer] /path/to/current
So long as no |:tcd|, |:lcd| or |:bcd| command has been used, all windows
share the same current directory. Using a command to jump to another window
doesn't change anything for the current directory.
When |:lcd| has been used for a window, the specified directory becomes the
current directory for that window. Windows where the |:lcd| command has not
been used stick to the global or tab-local directory. When jumping to another
window the current directory is changed to the last specified local current
directory. If none was specified, the global or tab-local directory is used.
When creating a new window it inherits the local directory of the current window.
been used stick to the buffer-local, tab-local or global directory. When
jumping to another window the current directory is changed to the last
specified local current directory. If none was specified, the buffer-local,
tab-local or global directory is used. When creating a new window it
inherits the local directory of the current window.
When |:bcd| has been used for a buffer, the specified directory becomes the
current directory whenever that buffer is entered, unless the window has its
own window-local directory. A new buffer created with |:edit| or |:enew|
inherits the buffer-local directory of the previous current buffer.
When changing tabs the same behaviour applies. If the current tab has no
local working directory the global working directory is used.

View File

@@ -1181,6 +1181,8 @@ Tag Command Action ~
|:ball| :ba[ll] open a window for each buffer in the buffer list
|:badd| :bad[d] add buffer to the buffer list
|:balt| :balt like ":badd" but also set the alternate file
|:bcd| :bc[d] change directory for buffer
|:bchdir| :bch[dir] change directory for buffer
|:bdelete| :bd[elete] remove a buffer from the buffer list
|:belowright| :bel[owright] make split window appear right or below
|:bfirst| :bf[irst] go to first buffer in the buffer list

View File

@@ -312,6 +312,7 @@ Command-line:
is open. The |cmdwin-char| is shown via 'statuscolumn'.
Commands:
- |:bcd|
- |:checkhealth|
- |:connect|
- |:detach|

View File

@@ -3774,9 +3774,8 @@ getcwd([{winnr} [, {tabnr} [, {bufnr}]]])
directory of the window indicated by {winnr}.
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the
working directory of that buffer is returned. If {bufnr} is
-1, it is ignored, and the global working directory is
returned.
working directory of that buffer is returned. An argument may
be -1 only if all preceding arguments are -1.
Examples of buffer usage: >vim
getcwd(-1, -1, 0) " Get current buffer's directory
getcwd(-1, -1, 3) " Get directory of buffer #3
@@ -5013,15 +5012,15 @@ has_key({dict}, {key}) *has_key()*
(`0|1`)
haslocaldir([{winnr} [, {tabnr} [, {bufnr} ]]]) *haslocaldir()*
Checks whether the window, tabpage or buffer has set a local
Checks whether the tabpage, window or buffer has set a local
working directory. Returns 1 when the window has set a local
path via |:lcd|, or when {winnr} is -1 and the tabpage has set
a local path via |:tcd|, or when {winnr} and {tabnr} are -1
and {bufnr} has set a local path via |:bcd|, otherwise 0.
Tabs, windows and buffers are identified by their respective
numbers, 0 means current tab or window. Missing argument
implies 0. Thus the following are equivalent: >vim
numbers, 0 means current tab, window or buffer. Missing
argument implies 0. Thus the following are equivalent: >vim
echo haslocaldir()
echo haslocaldir(0)
echo haslocaldir(0, 0)
@@ -5030,7 +5029,8 @@ haslocaldir([{winnr} [, {tabnr} [, {bufnr} ]]]) *haslocaldir()*
{winnr} is a |window-number| or |window-ID|.
If {winnr} is -1 it is ignored, only the tab is resolved.
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and
only the buffer is resolved.
only the buffer is resolved. An argument may be -1 only if
all preceding arguments are -1.
Examples of buffer usage: >vim
haslocaldir(-1, -1, 0) " Current buf has a local directory?
haslocaldir(-1, -1, 3) " Buf #3 has a local directory?

View File

@@ -3341,9 +3341,8 @@ function vim.fn.getcursorcharpos(winid) end
--- directory of the window indicated by {winnr}.
---
--- If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the
--- working directory of that buffer is returned. If {bufnr} is
--- -1, it is ignored, and the global working directory is
--- returned.
--- working directory of that buffer is returned. An argument may
--- be -1 only if all preceding arguments are -1.
--- Examples of buffer usage: >vim
--- getcwd(-1, -1, 0) " Get current buffer's directory
--- getcwd(-1, -1, 3) " Get directory of buffer #3
@@ -4462,15 +4461,15 @@ function vim.fn.has(feature) end
--- @return 0|1
function vim.fn.has_key(dict, key) end
--- Checks whether the window, tabpage or buffer has set a local
--- Checks whether the tabpage, window or buffer has set a local
--- working directory. Returns 1 when the window has set a local
--- path via |:lcd|, or when {winnr} is -1 and the tabpage has set
--- a local path via |:tcd|, or when {winnr} and {tabnr} are -1
--- and {bufnr} has set a local path via |:bcd|, otherwise 0.
---
--- Tabs, windows and buffers are identified by their respective
--- numbers, 0 means current tab or window. Missing argument
--- implies 0. Thus the following are equivalent: >vim
--- numbers, 0 means current tab, window or buffer. Missing
--- argument implies 0. Thus the following are equivalent: >vim
--- echo haslocaldir()
--- echo haslocaldir(0)
--- echo haslocaldir(0, 0)
@@ -4479,7 +4478,8 @@ function vim.fn.has_key(dict, key) end
--- {winnr} is a |window-number| or |window-ID|.
--- If {winnr} is -1 it is ignored, only the tab is resolved.
--- If {bufnr} is provided, {winnr} and {tabnr} must be -1 and
--- only the buffer is resolved.
--- only the buffer is resolved. An argument may be -1 only if
--- all preceding arguments are -1.
--- Examples of buffer usage: >vim
--- haslocaldir(-1, -1, 0) " Current buf has a local directory?
--- haslocaldir(-1, -1, 3) " Buf #3 has a local directory?

View File

@@ -4116,9 +4116,8 @@ M.funcs = {
directory of the window indicated by {winnr}.
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and the
working directory of that buffer is returned. If {bufnr} is
-1, it is ignored, and the global working directory is
returned.
working directory of that buffer is returned. An argument may
be -1 only if all preceding arguments are -1.
Examples of buffer usage: >vim
getcwd(-1, -1, 0) " Get current buffer's directory
getcwd(-1, -1, 3) " Get directory of buffer #3
@@ -5429,15 +5428,15 @@ M.funcs = {
args = { 0, 3 },
base = 1,
desc = [=[
Checks whether the window, tabpage or buffer has set a local
Checks whether the tabpage, window or buffer has set a local
working directory. Returns 1 when the window has set a local
path via |:lcd|, or when {winnr} is -1 and the tabpage has set
a local path via |:tcd|, or when {winnr} and {tabnr} are -1
and {bufnr} has set a local path via |:bcd|, otherwise 0.
Tabs, windows and buffers are identified by their respective
numbers, 0 means current tab or window. Missing argument
implies 0. Thus the following are equivalent: >vim
numbers, 0 means current tab, window or buffer. Missing
argument implies 0. Thus the following are equivalent: >vim
echo haslocaldir()
echo haslocaldir(0)
echo haslocaldir(0, 0)
@@ -5446,7 +5445,8 @@ M.funcs = {
{winnr} is a |window-number| or |window-ID|.
If {winnr} is -1 it is ignored, only the tab is resolved.
If {bufnr} is provided, {winnr} and {tabnr} must be -1 and
only the buffer is resolved.
only the buffer is resolved. An argument may be -1 only if
all preceding arguments are -1.
Examples of buffer usage: >vim
haslocaldir(-1, -1, 0) " Current buf has a local directory?
haslocaldir(-1, -1, 3) " Buf #3 has a local directory?

View File

@@ -681,6 +681,16 @@ static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope
}
}
// An argument may only be -1 if all preceding arguments are -1: `(2, 3, -1)` is an error.
bool explicit_scope = false;
for (int i = 0; i < argc; i++) {
explicit_scope = explicit_scope || argv[i] >= 0;
if (explicit_scope && argv[i] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return false;
}
}
// Narrowest requested scope. Imagine X >= 0:
switch (argc) {
case 0:
@@ -705,7 +715,7 @@ static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope
if (argv[kBufArg] >= 0) {
*scope = kCdScopeBuffer; // (-1, -1, X)
} else {
*scope = kCdScopeGlobal; // (..., ..., -1)
*scope = kCdScopeGlobal; // (-1, -1, -1)
}
break;
}
@@ -737,10 +747,6 @@ static bool getcwd_scope_args(typval_T *argvars, CdScope default_scope, CdScope
// Find the window in `tp` by number.
if (argv[kWinArg] >= 0) {
if (argv[kTabArg] < 0) {
emsg(_("E5001: Higher scope cannot be -1 if lower scope is >= 0."));
return false;
}
if (argv[kWinArg] > 0) {
*win = find_win_by_nr(&argvars[0], *tp);
if (*win == NULL) {

View File

@@ -6222,7 +6222,7 @@ static char **get_prevdir(CdScope scope)
/// Deal with the side effects of changing the current directory.
///
/// @param scope Scope of the function call (global, tab, buffer or window).
/// @param scope Scope of the function call (global, tab, window or buffer).
static void post_chdir(CdScope scope, bool trigger_dirchanged)
{
// Always overwrite the window-local CWD.
@@ -6275,9 +6275,9 @@ static void post_chdir(CdScope scope, bool trigger_dirchanged)
}
}
/// Change directory function used by :cd/:tcd/:bcd/:lcd Ex commands and the chdir() function.
/// Change directory function used by :cd/:tcd/:lcd/:bcd Ex commands and the chdir() function.
/// @param new_dir The directory to change to.
/// @param scope Scope of the function call (global, tab, buffer or window).
/// @param scope Scope of the function call (global, tab, window or buffer).
/// @return true if the directory is successfully changed.
bool changedir_func(char *new_dir, CdScope scope)
{
@@ -6333,7 +6333,7 @@ bool changedir_func(char *new_dir, CdScope scope)
return true;
}
/// ":cd", ":tcd", ":bcd", ":lcd", ":chdir", "tchdir", ":bchdir", and ":lchdir".
/// ":cd", ":tcd", ":lcd", ":bcd", ":chdir", ":tchdir", ":lchdir", and ":bchdir".
void ex_cd(exarg_T *eap)
{
char *new_dir = eap->arg;

View File

@@ -45,7 +45,7 @@ typedef enum {
/// What caused the current directory to change.
typedef enum {
kCdCauseOther = -1,
kCdCauseManual, ///< Using `:cd`, `:bcd`, `:tcd`, `:lcd` or `chdir()`.
kCdCauseManual, ///< Using `:cd`, `:tcd`, `:lcd`, `:bcd` or `chdir()`.
kCdCauseWindow, ///< Switching to another window.
kCdCauseBuffer, ///< Switching to another buffer.
kCdCauseAuto, ///< On 'autochdir'.

View File

@@ -438,32 +438,32 @@ for _, cmd in ipairs { 'getcwd', 'haslocaldir' } do
end)
it('validation', function()
local err474 = 'Vim(call):E474: Invalid argument'
eq(err474, pcall_err(command, ('call %s("some string")'):format(cmd)))
eq(err474, pcall_err(command, ('call %s(1.0)'):format(cmd)))
eq(err474, pcall_err(command, ('call %s([1, 2])'):format(cmd)))
eq(err474, pcall_err(command, ('call %s({"key": "value"})'):format(cmd)))
eq(err474, pcall_err(command, ('call %s(function("tr"))'):format(cmd)))
eq(err474, pcall_err(command, ('call %s(-2)'):format(cmd)))
local err474 = 'Vim:E474: Invalid argument'
eq(err474, pcall_err(call, cmd, 'some string'))
eq(err474, pcall_err(call, cmd, 1.5))
eq(err474, pcall_err(call, cmd, { 1, 2 }))
eq(err474, pcall_err(call, cmd, { key = 'value' }))
eq(err474, pcall_err(call, cmd, -2))
-- Funcref is not representable over RPC.
eq(
'Vim(call):E474: Invalid argument',
pcall_err(command, ('call %s(function("tr"))'):format(cmd))
)
-- -1 preceded by an argument >= 0
local err5001 = 'Vim:E5001: Higher scope cannot be -1 if lower scope is >= 0.'
eq(err5001, pcall_err(call, cmd, 0, -1))
eq(err5001, pcall_err(call, cmd, 2, 3, -1))
eq(err5001, pcall_err(call, cmd, -1, 0, -1))
eq(err5001, pcall_err(call, cmd, 0, -1, 0))
-- Buffer scope requires window and tab args to be -1.
local err5006 = 'Vim:E5006: Window and tab scope must be -1 when using buffer scope'
eq(err5006, pcall_err(call, cmd, 0, 0, 0))
eq(err5006, pcall_err(call, cmd, 1, 2, 3))
eq('Vim:E5007: Cannot find buffer number.', pcall_err(call, cmd, -1, -1, 99999))
eq(
'Vim(call):E5001: Higher scope cannot be -1 if lower scope is >= 0.',
pcall_err(command, ('call %s(0, -1)'):format(cmd))
)
-- Buffer scope requires window and tab arguments of -1.
local err5006 = 'Vim(call):E5006: Window and tab scope must be -1 when using buffer scope'
eq(err5006, pcall_err(command, ('call %s(0, 0, 0)'):format(cmd)))
eq(err5006, pcall_err(command, ('call %s(1, 2, 3)'):format(cmd)))
-- Nonexistent buffer.
eq(
'Vim(call):E5007: Cannot find buffer number.',
pcall_err(command, ('call %s(-1, -1, 99999)'):format(cmd))
)
-- Too many arguments.
eq(
('Vim(call):E118: Too many arguments for function: %s'):format(cmd),
pcall_err(command, ('call %s(0, 0, 0, 0)'):format(cmd))
('Vim:E118: Too many arguments for function: %s'):format(cmd),
pcall_err(call, cmd, 0, 0, 0, 0)
)
end)
end)