diff --git a/Cargo.lock b/Cargo.lock index 46ddb34c54..ca5298a9ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,6 +3172,7 @@ dependencies = [ "crossterm_winapi", "log", "lscolors", + "nix", "num-format", "strip-ansi-escapes", "sys-locale", diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 7eb84c257a..80a92438d0 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -23,7 +23,10 @@ use nu_protocol::{ report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value, NU_VARIABLE_ID, }; -use nu_utils::utils::perf; +use nu_utils::{ + filesystem::{have_permission, PermissionResult}, + utils::perf, +}; use reedline::{ CursorConfig, CwdAwareHinter, DefaultCompleter, EditCommand, Emacs, FileBackedHistory, HistorySessionId, Reedline, SqliteBackedHistory, Vi, @@ -761,6 +764,16 @@ fn do_auto_cd( path.to_string_lossy().to_string() }; + if let PermissionResult::PermissionDenied(reason) = have_permission(path.clone()) { + report_error_new( + engine_state, + &ShellError::IOError { + msg: format!("Cannot change directory to {path}: {reason}"), + }, + ); + return; + } + stack.add_env_var("OLDPWD".into(), Value::string(cwd.clone(), Span::unknown())); //FIXME: this only changes the current scope, but instead this environment variable diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index f46971bace..4dcfc46884 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -1,21 +1,5 @@ use nu_engine::{command_prelude::*, current_dir}; -use std::path::Path; -#[cfg(unix)] -use { - crate::filesystem::util::users, - nix::{ - sys::stat::{mode_t, Mode}, - unistd::{Gid, Uid}, - }, - std::os::unix::fs::MetadataExt, -}; - -// The result of checking whether we have permission to cd to a directory -#[derive(Debug)] -enum PermissionResult<'a> { - PermissionOk, - PermissionDenied(&'a str), -} +use nu_utils::filesystem::{have_permission, PermissionResult}; #[derive(Clone)] pub struct Cd; @@ -149,89 +133,3 @@ impl Command for Cd { ] } } - -// TODO: Maybe we should use file_attributes() from https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html -// More on that here: https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants -#[cfg(windows)] -fn have_permission(dir: impl AsRef) -> PermissionResult<'static> { - match dir.as_ref().read_dir() { - Err(e) => { - if matches!(e.kind(), std::io::ErrorKind::PermissionDenied) { - PermissionResult::PermissionDenied("Folder is unable to be read") - } else { - PermissionResult::PermissionOk - } - } - Ok(_) => PermissionResult::PermissionOk, - } -} - -#[cfg(unix)] -fn have_permission(dir: impl AsRef) -> PermissionResult<'static> { - match dir.as_ref().metadata() { - Ok(metadata) => { - let mode = Mode::from_bits_truncate(metadata.mode() as mode_t); - let current_user_uid = users::get_current_uid(); - if current_user_uid.is_root() { - return PermissionResult::PermissionOk; - } - let current_user_gid = users::get_current_gid(); - let owner_user = Uid::from_raw(metadata.uid()); - let owner_group = Gid::from_raw(metadata.gid()); - match ( - current_user_uid == owner_user, - current_user_gid == owner_group, - ) { - (true, _) => { - if mode.contains(Mode::S_IXUSR) { - PermissionResult::PermissionOk - } else { - PermissionResult::PermissionDenied( - "You are the owner but do not have execute permission", - ) - } - } - (false, true) => { - if mode.contains(Mode::S_IXGRP) { - PermissionResult::PermissionOk - } else { - PermissionResult::PermissionDenied( - "You are in the group but do not have execute permission", - ) - } - } - (false, false) => { - if mode.contains(Mode::S_IXOTH) - || (mode.contains(Mode::S_IXGRP) - && any_group(current_user_gid, owner_group)) - { - PermissionResult::PermissionOk - } else { - PermissionResult::PermissionDenied( - "You are neither the owner, in the group, nor the super user and do not have permission", - ) - } - } - } - } - Err(_) => PermissionResult::PermissionDenied("Could not retrieve file metadata"), - } -} - -#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] -fn any_group(_current_user_gid: Gid, owner_group: Gid) -> bool { - users::current_user_groups() - .unwrap_or_default() - .contains(&owner_group) -} - -#[cfg(all( - unix, - not(any(target_os = "linux", target_os = "freebsd", target_os = "android")) -))] -fn any_group(current_user_gid: Gid, owner_group: Gid) -> bool { - users::get_current_username() - .and_then(|name| users::get_user_groups(&name, current_user_gid)) - .unwrap_or_default() - .contains(&owner_group) -} diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 0def2b6fd0..c9facf624a 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -509,7 +509,7 @@ pub(crate) fn dir_entry_dict( #[cfg(unix)] { - use crate::filesystem::util::users; + use nu_utils::filesystem::users; use std::os::unix::fs::MetadataExt; let mode = md.permissions().mode(); diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs index 4491b8ea6d..d2b68ac58b 100644 --- a/crates/nu-command/src/filesystem/util.rs +++ b/crates/nu-command/src/filesystem/util.rs @@ -90,114 +90,6 @@ pub fn is_older(src: &Path, dst: &Path) -> Option { } } -#[cfg(unix)] -pub mod users { - use nix::unistd::{Gid, Group, Uid, User}; - - pub fn get_user_by_uid(uid: Uid) -> Option { - User::from_uid(uid).ok().flatten() - } - - pub fn get_group_by_gid(gid: Gid) -> Option { - Group::from_gid(gid).ok().flatten() - } - - pub fn get_current_uid() -> Uid { - Uid::current() - } - - pub fn get_current_gid() -> Gid { - Gid::current() - } - - #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] - pub fn get_current_username() -> Option { - get_user_by_uid(get_current_uid()).map(|user| user.name) - } - - #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] - pub fn current_user_groups() -> Option> { - if let Ok(mut groups) = nix::unistd::getgroups() { - groups.sort_unstable_by_key(|id| id.as_raw()); - groups.dedup(); - Some(groups) - } else { - None - } - } - - /// Returns groups for a provided user name and primary group id. - /// - /// # libc functions used - /// - /// - [`getgrouplist`](https://docs.rs/libc/*/libc/fn.getgrouplist.html) - /// - /// # Examples - /// - /// ```ignore - /// use users::get_user_groups; - /// - /// for group in get_user_groups("stevedore", 1001).expect("Error looking up groups") { - /// println!("User is a member of group #{group}"); - /// } - /// ``` - #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] - pub fn get_user_groups(username: &str, gid: Gid) -> Option> { - use nix::libc::{c_int, gid_t}; - use std::ffi::CString; - - // MacOS uses i32 instead of gid_t in getgrouplist for unknown reasons - #[cfg(target_os = "macos")] - let mut buff: Vec = vec![0; 1024]; - #[cfg(not(target_os = "macos"))] - let mut buff: Vec = vec![0; 1024]; - - let name = CString::new(username).ok()?; - - let mut count = buff.len() as c_int; - - // MacOS uses i32 instead of gid_t in getgrouplist for unknown reasons - // SAFETY: - // int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups); - // - // `name` is valid CStr to be `const char*` for `user` - // every valid value will be accepted for `group` - // The capacity for `*groups` is passed in as `*ngroups` which is the buffer max length/capacity (as we initialize with 0) - // Following reads from `*groups`/`buff` will only happen after `buff.truncate(*ngroups)` - #[cfg(target_os = "macos")] - let res = unsafe { - nix::libc::getgrouplist( - name.as_ptr(), - gid.as_raw() as i32, - buff.as_mut_ptr(), - &mut count, - ) - }; - - #[cfg(not(target_os = "macos"))] - let res = unsafe { - nix::libc::getgrouplist(name.as_ptr(), gid.as_raw(), buff.as_mut_ptr(), &mut count) - }; - - if res < 0 { - None - } else { - buff.truncate(count as usize); - buff.sort_unstable(); - buff.dedup(); - // allow trivial cast: on macos i is i32, on linux it's already gid_t - #[allow(trivial_numeric_casts)] - Some( - buff.into_iter() - .map(|id| Gid::from_raw(id as gid_t)) - .filter_map(get_group_by_gid) - .map(|group| group.gid) - .collect(), - ) - } - } -} - /// Get rest arguments from given `call`, starts with `starting_pos`. /// /// It's similar to `call.rest`, except that it always returns NuGlob. And if input argument has diff --git a/crates/nu-utils/Cargo.toml b/crates/nu-utils/Cargo.toml index 786f3b47b7..6ffb99f10e 100644 --- a/crates/nu-utils/Cargo.toml +++ b/crates/nu-utils/Cargo.toml @@ -26,3 +26,6 @@ unicase = "2.7.0" [target.'cfg(windows)'.dependencies] crossterm_winapi = "0.9" + +[target.'cfg(unix)'.dependencies] +nix = { workspace = true, default-features = false, features = ["user"] } \ No newline at end of file diff --git a/crates/nu-utils/src/filesystem.rs b/crates/nu-utils/src/filesystem.rs new file mode 100644 index 0000000000..588f0fccff --- /dev/null +++ b/crates/nu-utils/src/filesystem.rs @@ -0,0 +1,210 @@ +use std::path::Path; +#[cfg(unix)] +use { + nix::{ + sys::stat::{mode_t, Mode}, + unistd::{Gid, Uid}, + }, + std::os::unix::fs::MetadataExt, +}; + +// The result of checking whether we have permission to cd to a directory +#[derive(Debug)] +pub enum PermissionResult<'a> { + PermissionOk, + PermissionDenied(&'a str), +} + +// TODO: Maybe we should use file_attributes() from https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html +// More on that here: https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants +#[cfg(windows)] +pub fn have_permission(dir: impl AsRef) -> PermissionResult<'static> { + match dir.as_ref().read_dir() { + Err(e) => { + if matches!(e.kind(), std::io::ErrorKind::PermissionDenied) { + PermissionResult::PermissionDenied("Folder is unable to be read") + } else { + PermissionResult::PermissionOk + } + } + Ok(_) => PermissionResult::PermissionOk, + } +} + +#[cfg(unix)] +pub fn have_permission(dir: impl AsRef) -> PermissionResult<'static> { + match dir.as_ref().metadata() { + Ok(metadata) => { + let mode = Mode::from_bits_truncate(metadata.mode() as mode_t); + let current_user_uid = users::get_current_uid(); + if current_user_uid.is_root() { + return PermissionResult::PermissionOk; + } + let current_user_gid = users::get_current_gid(); + let owner_user = Uid::from_raw(metadata.uid()); + let owner_group = Gid::from_raw(metadata.gid()); + match ( + current_user_uid == owner_user, + current_user_gid == owner_group, + ) { + (true, _) => { + if mode.contains(Mode::S_IXUSR) { + PermissionResult::PermissionOk + } else { + PermissionResult::PermissionDenied( + "You are the owner but do not have execute permission", + ) + } + } + (false, true) => { + if mode.contains(Mode::S_IXGRP) { + PermissionResult::PermissionOk + } else { + PermissionResult::PermissionDenied( + "You are in the group but do not have execute permission", + ) + } + } + (false, false) => { + if mode.contains(Mode::S_IXOTH) + || (mode.contains(Mode::S_IXGRP) + && any_group(current_user_gid, owner_group)) + { + PermissionResult::PermissionOk + } else { + PermissionResult::PermissionDenied( + "You are neither the owner, in the group, nor the super user and do not have permission", + ) + } + } + } + } + Err(_) => PermissionResult::PermissionDenied("Could not retrieve file metadata"), + } +} + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] +fn any_group(_current_user_gid: Gid, owner_group: Gid) -> bool { + users::current_user_groups() + .unwrap_or_default() + .contains(&owner_group) +} + +#[cfg(all( + unix, + not(any(target_os = "linux", target_os = "freebsd", target_os = "android")) +))] +fn any_group(current_user_gid: Gid, owner_group: Gid) -> bool { + users::get_current_username() + .and_then(|name| users::get_user_groups(&name, current_user_gid)) + .unwrap_or_default() + .contains(&owner_group) +} + +#[cfg(unix)] +pub mod users { + use nix::unistd::{Gid, Group, Uid, User}; + + pub fn get_user_by_uid(uid: Uid) -> Option { + User::from_uid(uid).ok().flatten() + } + + pub fn get_group_by_gid(gid: Gid) -> Option { + Group::from_gid(gid).ok().flatten() + } + + pub fn get_current_uid() -> Uid { + Uid::current() + } + + pub fn get_current_gid() -> Gid { + Gid::current() + } + + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] + pub fn get_current_username() -> Option { + get_user_by_uid(get_current_uid()).map(|user| user.name) + } + + #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] + pub fn current_user_groups() -> Option> { + if let Ok(mut groups) = nix::unistd::getgroups() { + groups.sort_unstable_by_key(|id| id.as_raw()); + groups.dedup(); + Some(groups) + } else { + None + } + } + + /// Returns groups for a provided user name and primary group id. + /// + /// # libc functions used + /// + /// - [`getgrouplist`](https://docs.rs/libc/*/libc/fn.getgrouplist.html) + /// + /// # Examples + /// + /// ```ignore + /// use users::get_user_groups; + /// + /// for group in get_user_groups("stevedore", 1001).expect("Error looking up groups") { + /// println!("User is a member of group #{group}"); + /// } + /// ``` + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] + pub fn get_user_groups(username: &str, gid: Gid) -> Option> { + use nix::libc::{c_int, gid_t}; + use std::ffi::CString; + + // MacOS uses i32 instead of gid_t in getgrouplist for unknown reasons + #[cfg(target_os = "macos")] + let mut buff: Vec = vec![0; 1024]; + #[cfg(not(target_os = "macos"))] + let mut buff: Vec = vec![0; 1024]; + + let name = CString::new(username).ok()?; + + let mut count = buff.len() as c_int; + + // MacOS uses i32 instead of gid_t in getgrouplist for unknown reasons + // SAFETY: + // int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups); + // + // `name` is valid CStr to be `const char*` for `user` + // every valid value will be accepted for `group` + // The capacity for `*groups` is passed in as `*ngroups` which is the buffer max length/capacity (as we initialize with 0) + // Following reads from `*groups`/`buff` will only happen after `buff.truncate(*ngroups)` + #[cfg(target_os = "macos")] + let res = unsafe { + nix::libc::getgrouplist( + name.as_ptr(), + gid.as_raw() as i32, + buff.as_mut_ptr(), + &mut count, + ) + }; + + #[cfg(not(target_os = "macos"))] + let res = unsafe { + nix::libc::getgrouplist(name.as_ptr(), gid.as_raw(), buff.as_mut_ptr(), &mut count) + }; + + if res < 0 { + None + } else { + buff.truncate(count as usize); + buff.sort_unstable(); + buff.dedup(); + // allow trivial cast: on macos i is i32, on linux it's already gid_t + #[allow(trivial_numeric_casts)] + Some( + buff.into_iter() + .map(|id| Gid::from_raw(id as gid_t)) + .filter_map(get_group_by_gid) + .map(|group| group.gid) + .collect(), + ) + } + } +} diff --git a/crates/nu-utils/src/lib.rs b/crates/nu-utils/src/lib.rs index 575704dc81..00dcf36454 100644 --- a/crates/nu-utils/src/lib.rs +++ b/crates/nu-utils/src/lib.rs @@ -2,6 +2,7 @@ mod casing; pub mod ctrl_c; mod deansi; pub mod emoji; +pub mod filesystem; pub mod locale; pub mod utils; @@ -16,3 +17,6 @@ pub use deansi::{ strip_ansi_likely, strip_ansi_string_likely, strip_ansi_string_unlikely, strip_ansi_unlikely, }; pub use emoji::contains_emoji; + +#[cfg(unix)] +pub use filesystem::users;