莯凛 07be33c119
fix(nu-command): support ACL, SELinux, e.g. in cd have_permission check (#15360)
fixes #8095


# Description


This approach is a bit straightforward, call access() check with the
flag `X_OK`.

Zsh[^1], Fish perform this check by the same approach.

[^1]:
435cb1b748/Src/exec.c (L6406)

It could also avoid manual xattrs check on other *nix platforms.

BTW, the execution bit for directories in *nix world means permission to
access it's content,
while the read bit means to list it's content. [^0]

[^0]: https://superuser.com/a/169418

# User-Facing Changes

Users could face less permission check bugs in their `cd` usage.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

---------

Co-authored-by: Stefan Holderbach <sholderbach@users.noreply.github.com>
2025-03-27 14:23:41 +01:00

171 lines
6.4 KiB
Rust

use std::path::PathBuf;
use nu_engine::command_prelude::*;
use nu_protocol::shell_error::{self, io::IoError};
use nu_utils::filesystem::{have_permission, PermissionResult};
#[derive(Clone)]
pub struct Cd;
impl Command for Cd {
fn name(&self) -> &str {
"cd"
}
fn description(&self) -> &str {
"Change directory."
}
fn search_terms(&self) -> Vec<&str> {
vec!["change", "directory", "dir", "folder", "switch"]
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("cd")
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.switch("physical", "use the physical directory structure; resolve symbolic links before processing instances of ..", Some('P'))
.optional("path", SyntaxShape::Directory, "The path to change to.")
.allow_variants_without_examples(true)
.category(Category::FileSystem)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let physical = call.has_flag(engine_state, stack, "physical")?;
let path_val: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
// If getting PWD failed, default to the home directory. The user can
// use `cd` to reset PWD to a good state.
let cwd = engine_state
.cwd(Some(stack))
.ok()
.or_else(nu_path::home_dir)
.map(|path| path.into_std_path_buf())
.unwrap_or_default();
let path_val = {
if let Some(path) = path_val {
Some(Spanned {
item: nu_utils::strip_ansi_string_unlikely(path.item),
span: path.span,
})
} else {
path_val
}
};
let path = match path_val {
Some(v) => {
if v.item == "-" {
if let Some(oldpwd) = stack.get_env_var(engine_state, "OLDPWD") {
oldpwd.to_path()?
} else {
cwd
}
} else {
// Trim whitespace from the end of path.
let path_no_whitespace =
&v.item.trim_end_matches(|x| matches!(x, '\x09'..='\x0d'));
// If `--physical` is specified, canonicalize the path; otherwise expand the path.
if physical {
if let Ok(path) = nu_path::canonicalize_with(path_no_whitespace, &cwd) {
if !path.is_dir() {
return Err(shell_error::io::IoError::new(
shell_error::io::ErrorKind::Std(
std::io::ErrorKind::NotADirectory,
),
v.span,
None,
)
.into());
};
path
} else {
return Err(shell_error::io::IoError::new(
ErrorKind::DirectoryNotFound,
v.span,
PathBuf::from(path_no_whitespace),
)
.into());
}
} else {
let path = nu_path::expand_path_with(path_no_whitespace, &cwd, true);
if !path.exists() {
return Err(shell_error::io::IoError::new(
ErrorKind::DirectoryNotFound,
v.span,
PathBuf::from(path_no_whitespace),
)
.into());
};
if !path.is_dir() {
return Err(shell_error::io::IoError::new(
shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotADirectory),
v.span,
path,
)
.into());
};
path
}
}
}
None => nu_path::expand_tilde("~"),
};
// Set OLDPWD.
// We're using `Stack::get_env_var()` instead of `EngineState::cwd()` to avoid a conversion roundtrip.
if let Some(oldpwd) = stack.get_env_var(engine_state, "PWD") {
stack.add_env_var("OLDPWD".into(), oldpwd.clone())
}
match have_permission(&path) {
//FIXME: this only changes the current scope, but instead this environment variable
//should probably be a block that loads the information from the state in the overlay
PermissionResult::PermissionOk => {
stack.set_cwd(path)?;
Ok(PipelineData::empty())
}
PermissionResult::PermissionDenied => {
Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into())
}
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Change to your home directory",
example: r#"cd ~"#,
result: None,
},
Example {
description: r#"Change to the previous working directory (same as "cd $env.OLDPWD")"#,
example: r#"cd -"#,
result: None,
},
Example {
description: "Changing directory with a custom command requires 'def --env'",
example: r#"def --env gohome [] { cd ~ }"#,
result: None,
},
Example {
description: "Move two directories up in the tree (the parent directory's parent). Additional dots can be added for additional levels.",
example: r#"cd ..."#,
result: None,
},
Example {
description: "The cd command itself is often optional. Simply entering a path to a directory will cd to it.",
example: r#"/home"#,
result: None,
},
]
}
}