Always pass in the working directory in path_get_cdpath
If the user is in a directory which has been unlinked, it is possible for the path .. to not exist, relative to the working directory. Always pass in the working directory (potentially virtual) to path_get_cdpath; this ensures we check absolute paths and are immune from issues if the working directory has been unlinked. Also introduce a new function path_normalize_for_cd which normalizes the "join point" of a path and a working directory. This allows us to 'cd' out of a non-existent directory, but not cd into such a directory. Fixes #5341
This commit is contained in:
parent
1ab84ac62a
commit
a8ce7bad7b
@ -47,7 +47,7 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
|||||||
dir_in = maybe_dir_in->as_string();
|
dir_in = maybe_dir_in->as_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path_get_cdpath(dir_in, &dir)) {
|
if (!path_get_cdpath(dir_in, &dir, env_get_pwd_slash())) {
|
||||||
if (errno == ENOTDIR) {
|
if (errno == ENOTDIR) {
|
||||||
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
|
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
|
||||||
} else if (errno == ENOENT) {
|
} else if (errno == ENOENT) {
|
||||||
@ -65,9 +65,7 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
|||||||
return STATUS_CMD_ERROR;
|
return STATUS_CMD_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepend the PWD if we don't start with a slash, and then normalize the directory.
|
wcstring norm_dir = normalize_path(dir);
|
||||||
wcstring norm_dir =
|
|
||||||
normalize_path(string_prefixes_string(L"/", dir) ? dir : env_get_pwd_slash() + dir);
|
|
||||||
|
|
||||||
if (wchdir(norm_dir) != 0) {
|
if (wchdir(norm_dir) != 0) {
|
||||||
struct stat buffer;
|
struct stat buffer;
|
||||||
@ -75,10 +73,10 @@ int builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv) {
|
|||||||
|
|
||||||
status = wstat(dir, &buffer);
|
status = wstat(dir, &buffer);
|
||||||
if (!status && S_ISDIR(buffer.st_mode)) {
|
if (!status && S_ISDIR(buffer.st_mode)) {
|
||||||
streams.err.append_format(_(L"%ls: Permission denied: '%ls'\n"), cmd, dir.c_str());
|
streams.err.append_format(_(L"%ls: Permission denied: '%ls'\n"), cmd, dir_in.c_str());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir.c_str());
|
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shell_is_interactive()) {
|
if (!shell_is_interactive()) {
|
||||||
|
@ -4916,6 +4916,19 @@ void test_normalize_path() {
|
|||||||
do_test(normalize_path(L"foo/../foo") == L"foo");
|
do_test(normalize_path(L"foo/../foo") == L"foo");
|
||||||
do_test(normalize_path(L"foo/../foo/") == L"foo");
|
do_test(normalize_path(L"foo/../foo/") == L"foo");
|
||||||
do_test(normalize_path(L"foo/././bar/.././baz") == L"foo/baz");
|
do_test(normalize_path(L"foo/././bar/.././baz") == L"foo/baz");
|
||||||
|
|
||||||
|
do_test(path_normalize_for_cd(L"/", L"..") == L"/..");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc/", L"..") == L"/");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc/def/", L"..") == L"/abc");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc/def/", L"../..") == L"/");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc///def/", L"../..") == L"/");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc///def/", L"../..") == L"/");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc///def///", L"../..") == L"/");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc///def///", L"..") == L"/abc");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc///def///", L"..") == L"/abc");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc/def/", L"./././/..") == L"/abc");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc/def/", L"../../../") == L"/../");
|
||||||
|
do_test(path_normalize_for_cd(L"/abc/def/", L"../ghi/..") == L"/abc/ghi/..");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main test.
|
/// Main test.
|
||||||
|
@ -1021,7 +1021,7 @@ static bool command_is_valid(const wcstring &cmd, enum parse_statement_decoratio
|
|||||||
|
|
||||||
// Implicit cd
|
// Implicit cd
|
||||||
if (!is_valid && implicit_cd_ok) {
|
if (!is_valid && implicit_cd_ok) {
|
||||||
is_valid = path_can_be_implicit_cd(cmd, NULL, working_directory, vars);
|
is_valid = path_can_be_implicit_cd(cmd, working_directory, NULL, vars);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return what we got.
|
// Return what we got.
|
||||||
|
@ -809,7 +809,8 @@ parse_execution_result_t parse_execution_context_t::populate_plain_process(
|
|||||||
!args.try_get_child<g::redirection, 0>()) {
|
!args.try_get_child<g::redirection, 0>()) {
|
||||||
// Ok, no arguments or redirections; check to see if the command is a directory.
|
// Ok, no arguments or redirections; check to see if the command is a directory.
|
||||||
wcstring implicit_cd_path;
|
wcstring implicit_cd_path;
|
||||||
use_implicit_cd = path_can_be_implicit_cd(cmd, &implicit_cd_path);
|
use_implicit_cd =
|
||||||
|
path_can_be_implicit_cd(cmd, env_get_pwd_slash(), &implicit_cd_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ bool path_get_cdpath(const wcstring &dir, wcstring *out, const wcstring &wd,
|
|||||||
int err = ENOENT;
|
int err = ENOENT;
|
||||||
if (dir.empty()) return false;
|
if (dir.empty()) return false;
|
||||||
|
|
||||||
assert(wd.empty() || wd.back() == L'/');
|
assert(!wd.empty() && wd.back() == L'/');
|
||||||
|
|
||||||
wcstring_list_t paths;
|
wcstring_list_t paths;
|
||||||
if (dir.at(0) == L'/') {
|
if (dir.at(0) == L'/') {
|
||||||
@ -171,10 +171,7 @@ bool path_get_cdpath(const wcstring &dir, wcstring *out, const wcstring &wd,
|
|||||||
} else if (string_prefixes_string(L"./", dir) || string_prefixes_string(L"../", dir) ||
|
} else if (string_prefixes_string(L"./", dir) || string_prefixes_string(L"../", dir) ||
|
||||||
dir == L"." || dir == L"..") {
|
dir == L"." || dir == L"..") {
|
||||||
// Path is relative to the working directory.
|
// Path is relative to the working directory.
|
||||||
wcstring path;
|
paths.push_back(path_normalize_for_cd(wd, dir));
|
||||||
if (!wd.empty()) path.append(wd);
|
|
||||||
path.append(dir);
|
|
||||||
paths.push_back(path);
|
|
||||||
} else {
|
} else {
|
||||||
// Respect CDPATH.
|
// Respect CDPATH.
|
||||||
wcstring_list_t cdpathsv;
|
wcstring_list_t cdpathsv;
|
||||||
@ -218,7 +215,7 @@ bool path_get_cdpath(const wcstring &dir, wcstring *out, const wcstring &wd,
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool path_can_be_implicit_cd(const wcstring &path, wcstring *out_path, const wcstring &wd,
|
bool path_can_be_implicit_cd(const wcstring &path, const wcstring &wd, wcstring *out_path,
|
||||||
const env_vars_snapshot_t &vars) {
|
const env_vars_snapshot_t &vars) {
|
||||||
wcstring exp_path = path;
|
wcstring exp_path = path;
|
||||||
expand_tilde(exp_path);
|
expand_tilde(exp_path);
|
||||||
|
@ -56,19 +56,17 @@ wcstring_list_t path_get_paths(const wcstring &cmd);
|
|||||||
///
|
///
|
||||||
/// \param dir The name of the directory.
|
/// \param dir The name of the directory.
|
||||||
/// \param out_or_NULL If non-NULL, return the path to the resolved directory
|
/// \param out_or_NULL If non-NULL, return the path to the resolved directory
|
||||||
/// \param wd The working directory, or NULL to use the default. The working directory should have a
|
/// \param wd The working directory, which should have a slash appended at the end.
|
||||||
/// slash appended at the end.
|
|
||||||
/// \param vars The environment variable snapshot to use (for the CDPATH variable)
|
/// \param vars The environment variable snapshot to use (for the CDPATH variable)
|
||||||
/// \return 0 if the command can not be found, the path of the command otherwise. The path should be
|
/// \return 0 if the command can not be found, the path of the command otherwise. The path should be
|
||||||
/// free'd with free().
|
/// free'd with free().
|
||||||
bool path_get_cdpath(const wcstring &dir, wcstring *out_or_NULL, const wcstring &wd = L"",
|
bool path_get_cdpath(const wcstring &dir, wcstring *out_or_NULL, const wcstring &wd,
|
||||||
const env_vars_snapshot_t &vars = env_vars_snapshot_t::current());
|
const env_vars_snapshot_t &vars = env_vars_snapshot_t::current());
|
||||||
|
|
||||||
/// Returns whether the path can be used for an implicit cd command; if so, also returns the path by
|
/// Returns whether the path can be used for an implicit cd command; if so, also returns the path by
|
||||||
/// reference (if desired). This requires it to start with one of the allowed prefixes (., .., ~)
|
/// reference (if desired). This requires it to start with one of the allowed prefixes (., .., ~)
|
||||||
/// and resolve to a directory.
|
/// and resolve to a directory.
|
||||||
bool path_can_be_implicit_cd(const wcstring &path, wcstring *out_path = NULL,
|
bool path_can_be_implicit_cd(const wcstring &path, const wcstring &wd, wcstring *out_path = NULL,
|
||||||
const wcstring &wd = L"",
|
|
||||||
const env_vars_snapshot_t &vars = env_vars_snapshot_t::current());
|
const env_vars_snapshot_t &vars = env_vars_snapshot_t::current());
|
||||||
|
|
||||||
/// Remove double slashes and trailing slashes from a path, e.g. transform foo//bar/ into foo/bar.
|
/// Remove double slashes and trailing slashes from a path, e.g. transform foo//bar/ into foo/bar.
|
||||||
|
@ -466,6 +466,50 @@ wcstring normalize_path(const wcstring &path) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wcstring path_normalize_for_cd(const wcstring &wd, const wcstring &path) {
|
||||||
|
// Fast paths.
|
||||||
|
const wchar_t sep = L'/';
|
||||||
|
assert(!wd.empty() && wd.front() == sep && wd.back() == sep &&
|
||||||
|
"Invalid working directory, it must start and end with /");
|
||||||
|
if (path.empty()) {
|
||||||
|
return wd;
|
||||||
|
} else if (path.front() == sep) {
|
||||||
|
return path;
|
||||||
|
} else if (path.front() != L'.') {
|
||||||
|
return wd + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split our strings by the sep.
|
||||||
|
wcstring_list_t wd_comps = split_string(wd, sep);
|
||||||
|
wcstring_list_t path_comps = split_string(path, sep);
|
||||||
|
|
||||||
|
// Remove empty segments from wd_comps.
|
||||||
|
// In particular this removes the leading and trailing empties.
|
||||||
|
wd_comps.erase(std::remove(wd_comps.begin(), wd_comps.end(), L""), wd_comps.end());
|
||||||
|
|
||||||
|
// Erase leading . and .. components from path_comps, popping from wd_comps as we go.
|
||||||
|
size_t erase_count = 0;
|
||||||
|
for (const wcstring &comp : path_comps) {
|
||||||
|
bool erase_it = false;
|
||||||
|
if (comp.empty() || comp == L".") {
|
||||||
|
erase_it = true;
|
||||||
|
} else if (comp == L".." && !wd_comps.empty()) {
|
||||||
|
erase_it = true;
|
||||||
|
wd_comps.pop_back();
|
||||||
|
}
|
||||||
|
if (erase_it) {
|
||||||
|
erase_count++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Append un-erased elements to wd_comps and join them, then prepend the leading /.
|
||||||
|
std::move(path_comps.begin() + erase_count, path_comps.end(), std::back_inserter(wd_comps));
|
||||||
|
wcstring result = join_strings(wd_comps, sep);
|
||||||
|
result.insert(0, 1, L'/');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
wcstring wdirname(const wcstring &path) {
|
wcstring wdirname(const wcstring &path) {
|
||||||
char *tmp = wcs2str(path);
|
char *tmp = wcs2str(path);
|
||||||
char *narrow_res = dirname(tmp);
|
char *narrow_res = dirname(tmp);
|
||||||
|
@ -74,6 +74,12 @@ maybe_t<wcstring> wrealpath(const wcstring &pathname);
|
|||||||
/// 3. Remove /./ in the middle.
|
/// 3. Remove /./ in the middle.
|
||||||
wcstring normalize_path(const wcstring &path);
|
wcstring normalize_path(const wcstring &path);
|
||||||
|
|
||||||
|
/// Given an input path \p path and a working directory \p wd, do a "normalizing join" in a way
|
||||||
|
/// appropriate for cd. That is, return effectively wd + path while resolving leading ../s from
|
||||||
|
/// path. The intent here is to allow 'cd' out of a directory which may no longer exist, without
|
||||||
|
/// allowing 'cd' into a directory that may not exist; see #5341.
|
||||||
|
wcstring path_normalize_for_cd(const wcstring &wd, const wcstring &path);
|
||||||
|
|
||||||
/// Wide character version of readdir().
|
/// Wide character version of readdir().
|
||||||
bool wreaddir(DIR *dir, wcstring &out_name);
|
bool wreaddir(DIR *dir, wcstring &out_name);
|
||||||
bool wreaddir_resolving(DIR *dir, const std::wstring &dir_path, wcstring &out_name,
|
bool wreaddir_resolving(DIR *dir, const std::wstring &dir_path, wcstring &out_name,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user