cd first, ask questions later (#7586)

cd: Just try to cd without checking first

Some filesystems are broken and error out on `stat(3)` of existing and
cd-able directories.

So we just try to `fchdir` and report errors later.

Fixes #7577.
This commit is contained in:
Fabian Homborg 2021-03-27 18:28:03 +01:00 committed by GitHub
parent 50398ea9f5
commit df53d1415d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 68 additions and 40 deletions

View File

@ -47,55 +47,72 @@ maybe_t<int> builtin_cd(parser_t &parser, io_streams_t &streams, wchar_t **argv)
}
wcstring pwd = parser.vars().get_pwd_slash();
maybe_t<wcstring> mdir = path_get_cdpath(dir_in, pwd, parser.vars());
if (!mdir) {
if (errno == ENOTDIR) {
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
} else if (errno == ENOENT) {
streams.err.append_format(_(L"%ls: The directory '%ls' does not exist\n"), cmd,
dir_in.c_str());
} else if (errno == EROTTEN) {
streams.err.append_format(_(L"%ls: '%ls' is a rotten symlink\n"), cmd, dir_in.c_str());
} else {
streams.err.append_format(_(L"%ls: Unknown error trying to locate directory '%ls'\n"),
cmd, dir_in.c_str());
}
auto dirs = path_apply_cdpath(dir_in, pwd, parser.vars());
if (dirs.empty()) {
streams.err.append_format(_(L"%ls: The directory '%ls' does not exist\n"), cmd,
dir_in.c_str());
if (!parser.is_interactive()) streams.err.append(parser.current_line());
return STATUS_CMD_ERROR;
}
const wcstring &dir = *mdir;
wcstring norm_dir = normalize_path(dir);
errno = 0;
auto best_errno = errno;
// We need to keep around the fd for this directory, in the parser.
autoclose_fd_t dir_fd(wopen_cloexec(norm_dir, O_RDONLY));
bool success = dir_fd.valid() && fchdir(dir_fd.fd()) == 0;
for (const auto &dir: dirs) {
wcstring norm_dir = normalize_path(dir);
if (!success) {
struct stat buffer;
int status;
// We need to keep around the fd for this directory, in the parser.
errno = 0;
autoclose_fd_t dir_fd(wopen_cloexec(norm_dir, O_RDONLY));
bool success = dir_fd.valid() && fchdir(dir_fd.fd()) == 0;
status = wstat(dir, &buffer);
if (!status && S_ISDIR(buffer.st_mode)) {
streams.err.append_format(_(L"%ls: Permission denied: '%ls'\n"), cmd, dir_in.c_str());
} else {
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
if (!success) {
// Some errors we skip and only report if nothing worked.
// ENOENT in particular is very low priority
// - if in another directory there was a *file* by the correct name
// we prefer *that* error because it's more specific
if (errno == ENOENT) {
if (!best_errno) best_errno = errno;
continue;
} else if (errno == ENOTDIR) {
best_errno = errno;
continue;
}
best_errno = errno;
break;
}
if (!parser.is_interactive()) {
streams.err.append(parser.current_line());
parser.libdata().cwd_fd = std::make_shared<const autoclose_fd_t>(std::move(dir_fd));
std::vector<event_t> evts;
parser.vars().set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, std::move(norm_dir), &evts);
for (const auto &evt : evts) {
event_fire(parser, evt);
}
return STATUS_CMD_ERROR;
return STATUS_CMD_OK;
}
parser.libdata().cwd_fd = std::make_shared<const autoclose_fd_t>(std::move(dir_fd));
std::vector<event_t> evts;
parser.vars().set_one(L"PWD", ENV_EXPORT | ENV_GLOBAL, std::move(norm_dir), &evts);
for (const auto &evt : evts) {
event_fire(parser, evt);
if (best_errno == ENOTDIR) {
streams.err.append_format(_(L"%ls: '%ls' is not a directory\n"), cmd, dir_in.c_str());
} else if (best_errno == ENOENT) {
streams.err.append_format(_(L"%ls: The directory '%ls' does not exist\n"), cmd,
dir_in.c_str());
} else if (best_errno == EROTTEN) {
streams.err.append_format(_(L"%ls: '%ls' is a rotten symlink\n"), cmd, dir_in.c_str());
} else if (best_errno == EACCES) {
streams.err.append_format(_(L"%ls: Permission denied: '%ls'\n"), cmd, dir_in.c_str());
} else {
errno = best_errno;
wperror(L"cd");
streams.err.append_format(_(L"%ls: Unknown error trying to locate directory '%ls'\n"),
cmd, dir_in.c_str());
}
return STATUS_CMD_OK;
if (!parser.is_interactive()) {
streams.err.append(parser.current_line());
}
return STATUS_CMD_ERROR;
}

View File

@ -126,11 +126,8 @@ wcstring_list_t path_get_paths(const wcstring &cmd, const environment_t &vars) {
return paths;
}
maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
wcstring_list_t path_apply_cdpath(const wcstring &dir, const wcstring &wd,
const environment_t &env_vars) {
int err = ENOENT;
if (dir.empty()) return none();
assert(!wd.empty() && wd.back() == L'/');
wcstring_list_t paths;
if (dir.at(0) == L'/') {
// Absolute path.
@ -170,6 +167,16 @@ maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
}
}
return paths;
}
maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
const environment_t &env_vars) {
int err = ENOENT;
if (dir.empty()) return none();
assert(!wd.empty() && wd.back() == L'/');
auto paths = path_apply_cdpath(dir, wd, env_vars);
for (const wcstring &dir : paths) {
struct stat buf;
if (wstat(dir, &buf) == 0) {

View File

@ -64,6 +64,10 @@ wcstring_list_t path_get_paths(const wcstring &cmd, const environment_t &vars);
maybe_t<wcstring> path_get_cdpath(const wcstring &dir, const wcstring &wd,
const environment_t &vars);
/// Returns the given directory with all CDPATH components applied.
wcstring_list_t path_apply_cdpath(const wcstring &dir, const wcstring &wd,
const environment_t &env_vars);
/// Returns the path resolved as an implicit cd command, or none() if none. This requires it to
/// start with one of the allowed prefixes (., .., ~) and resolve to a directory.
maybe_t<wcstring> path_as_implicit_cd(const wcstring &path, const wcstring &wd,