From 57c39437bc50992dfe11b104229db854cb912a18 Mon Sep 17 00:00:00 2001 From: Alexey Chernyshov Date: Tue, 14 Jul 2020 00:55:42 +0300 Subject: [PATCH] feat(directory): Show lock symbol if current directory is read only (#1298) Add feature to display icon if current directory is read-only. --- Cargo.lock | 17 ++++- Cargo.toml | 6 ++ docs/config/README.md | 16 ++-- src/configs/directory.rs | 6 +- src/modules/directory.rs | 27 +++++++ src/modules/utils/directory_nix.rs | 61 +++++++++++++++ src/modules/utils/directory_win.rs | 117 +++++++++++++++++++++++++++++ src/modules/utils/mod.rs | 6 ++ tests/testsuite/directory.rs | 15 +++- 9 files changed, 260 insertions(+), 11 deletions(-) create mode 100644 src/modules/utils/directory_nix.rs create mode 100644 src/modules/utils/directory_win.rs diff --git a/Cargo.lock b/Cargo.lock index e14d0d7d..345e0202 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ "lazycell", "libc", "mach", - "nix", + "nix 0.15.0", "num-traits", "uom", "winapi", @@ -651,6 +651,19 @@ dependencies = [ "void", ] +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "void", +] + [[package]] name = "nom" version = "5.1.2" @@ -1152,6 +1165,7 @@ dependencies = [ "git2", "log", "native-tls", + "nix 0.17.0", "nom", "once_cell", "open", @@ -1174,6 +1188,7 @@ dependencies = [ "unicode-segmentation", "unicode-width", "urlencoding", + "winapi", "yaml-rust", ] diff --git a/Cargo.toml b/Cargo.toml index 288b6926..bc087851 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,12 @@ quick-xml = "0.18.1" attohttpc = { version = "0.15.0", optional = true, default-features = false, features = ["tls", "form"] } native-tls = { version = "0.2", optional = true } +[target.'cfg(windows)'.dependencies] +winapi = { version = "0.3", features = ["winuser", "securitybaseapi", "processthreadsapi", "handleapi", "impl-default"]} + +[target.'cfg(not(windows))'.dependencies] +nix = "0.17.0" + [dev-dependencies] tempfile = "3.1.0" # More realiable than std::fs version on Windows diff --git a/docs/config/README.md b/docs/config/README.md index 1c8c5422..393bae4c 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -589,13 +589,15 @@ it would have been `nixpkgs/pkgs`. ### Options -| Option | Default | Description | -| ------------------- | -------------------- | -------------------------------------------------------------------------------- | -| `truncation_length` | `3` | The number of parent folders that the current directory should be truncated to. | -| `truncate_to_repo` | `true` | Whether or not to truncate to the root of the git repo that you're currently in. | -| `format` | `"[$path]($style) "` | The format for the module. | -| `style` | `"bold cyan"` | The style for the module. | -| `disabled` | `false` | Disables the `directory` module. | +| Variable | Default | Description | +| ------------------------ | ----------------------------------------------- | -------------------------------------------------------------------------------- | +| `truncation_length` | `3` | The number of parent folders that the current directory should be truncated to. | +| `truncate_to_repo` | `true` | Whether or not to truncate to the root of the git repo that you're currently in. | +| `format` | `"[$path]($style)[$lock_symbol]($lock_style) "` | The format for the module. | +| `style` | `"bold cyan"` | The style for the module. | +| `disabled` | `false` | Disables the `directory` module. | +| `read_only_symbol` | `"🔒"` | The symbol indicating current directory is read only. | +| `read_only_symbol_style` | `"red"` | The style for the read only symbol. |
This module has a few advanced configuration options that control how the directory is displayed. diff --git a/src/configs/directory.rs b/src/configs/directory.rs index 7dd1c579..fe414001 100644 --- a/src/configs/directory.rs +++ b/src/configs/directory.rs @@ -13,6 +13,8 @@ pub struct DirectoryConfig<'a> { pub format: &'a str, pub style: &'a str, pub disabled: bool, + pub read_only_symbol: &'a str, + pub read_only_symbol_style: &'a str, } impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> { @@ -23,9 +25,11 @@ impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> { fish_style_pwd_dir_length: 0, substitutions: HashMap::new(), use_logical_path: true, - format: "[$path]($style) ", + format: "[$path]($style)[$read_only]($read_only_style) ", style: "cyan bold", disabled: false, + read_only_symbol: "🔒", + read_only_symbol_style: "red", } } } diff --git a/src/modules/directory.rs b/src/modules/directory.rs index 31f48a2c..a2c397f1 100644 --- a/src/modules/directory.rs +++ b/src/modules/directory.rs @@ -1,3 +1,7 @@ +#[cfg(not(target_os = "windows"))] +use super::utils::directory_nix as directory_utils; +#[cfg(target_os = "windows")] +use super::utils::directory_win as directory_utils; use path_slash::PathExt; use std::collections::HashMap; use std::iter::FromIterator; @@ -91,15 +95,24 @@ pub fn module<'a>(context: &'a Context) -> Option> { String::from("") }; let final_dir_string = format!("{}{}", fish_prefix, truncated_dir_string); + let lock_symbol = String::from(config.read_only_symbol); let parsed = StringFormatter::new(config.format).and_then(|formatter| { formatter .map_style(|variable| match variable { "style" => Some(Ok(config.style)), + "read_only_style" => Some(Ok(config.read_only_symbol_style)), _ => None, }) .map(|variable| match variable { "path" => Some(Ok(&final_dir_string)), + "read_only" => { + if is_readonly_dir(current_dir.to_str()?) { + Some(Ok(&lock_symbol)) + } else { + None + } + } _ => None, }) .parse(None) @@ -116,6 +129,20 @@ pub fn module<'a>(context: &'a Context) -> Option> { Some(module) } +fn is_readonly_dir(path: &str) -> bool { + match directory_utils::is_write_allowed(path) { + Ok(res) => !res, + Err(e) => { + log::debug!( + "Failed to detemine read only status of directory '{}': {}", + path, + e + ); + false + } + } +} + /// Contract the root component of a path /// /// Replaces the `top_level_path` in a given `full_path` with the provided diff --git a/src/modules/utils/directory_nix.rs b/src/modules/utils/directory_nix.rs new file mode 100644 index 00000000..87f94b7e --- /dev/null +++ b/src/modules/utils/directory_nix.rs @@ -0,0 +1,61 @@ +use nix::sys::stat::Mode; +use nix::unistd::{Gid, Uid}; +use std::fs; +use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::PermissionsExt; + +/// Checks if the current user can write to the `folder_path`. +/// +/// It extracts Unix access rights from the directory and checks whether +/// 1) the current user is the owner of the directory and whether it has the write access +/// 2) the current user's primary group is the directory group owner whether if it has write access +/// 2a) (not implemented on macOS) one of the supplementary groups of the current user is the +/// directory group owner and whether it has write access +/// 3) 'others' part of the access mask has the write access +pub fn is_write_allowed(folder_path: &str) -> Result { + let meta = fs::metadata(folder_path).map_err(|_| "Unable to stat() directory")?; + let perms = meta.permissions().mode(); + + let euid = Uid::effective(); + if euid.is_root() { + return Ok(true); + } + if meta.uid() == euid.as_raw() { + Ok(perms & Mode::S_IWUSR.bits() as u32 != 0) + } else if (meta.gid() == Gid::effective().as_raw()) + || (get_supplementary_groups().contains(&meta.gid())) + { + Ok(perms & Mode::S_IWGRP.bits() as u32 != 0) + } else { + Ok(perms & Mode::S_IWOTH.bits() as u32 != 0) + } +} + +#[cfg(all(unix, not(target_os = "macos")))] +fn get_supplementary_groups() -> Vec { + match nix::unistd::getgroups() { + Err(_) => Vec::new(), + Ok(v) => v.into_iter().map(|i| i.as_raw()).collect(), + } +} + +#[cfg(all(unix, target_os = "macos"))] +fn get_supplementary_groups() -> Vec { + // at the moment nix crate does not provide it for macOS + Vec::new() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] + fn read_only_test() { + assert_eq!(is_write_allowed("/etc"), Ok(false)); + assert_eq!( + is_write_allowed("/i_dont_exist"), + Err("Unable to stat() directory") + ); + } +} diff --git a/src/modules/utils/directory_win.rs b/src/modules/utils/directory_win.rs new file mode 100644 index 00000000..6574be55 --- /dev/null +++ b/src/modules/utils/directory_win.rs @@ -0,0 +1,117 @@ +extern crate winapi; + +use std::ffi::OsStr; +use std::iter; +use std::mem; +use std::os::windows::ffi::OsStrExt; +use winapi::ctypes::c_void; +use winapi::shared::minwindef::{BOOL, DWORD}; +use winapi::um::handleapi; +use winapi::um::processthreadsapi; +use winapi::um::securitybaseapi; +use winapi::um::winnt::{ + SecurityImpersonation, DACL_SECURITY_INFORMATION, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, HANDLE, + OWNER_SECURITY_INFORMATION, PRIVILEGE_SET, PSECURITY_DESCRIPTOR, STANDARD_RIGHTS_READ, + TOKEN_DUPLICATE, TOKEN_IMPERSONATE, TOKEN_QUERY, +}; + +/// Checks if the current user has write access right to the `folder_path` +/// +/// First, the function extracts DACL from the given directory and then calls `AccessCheck` against +/// the current process access token and directory's security descriptor. +pub fn is_write_allowed(folder_path: &str) -> std::result::Result { + let folder_name: Vec = OsStr::new(folder_path) + .encode_wide() + .chain(iter::once(0)) + .collect(); + let mut length: DWORD = 0; + + let rc = unsafe { + securitybaseapi::GetFileSecurityW( + folder_name.as_ptr(), + OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + std::ptr::null_mut(), + 0, + &mut length, + ) + }; + if rc != 0 { + return Err( + "GetFileSecurityW returned non-zero when asked for the security descriptor size", + ); + } + + let mut buf: Vec = Vec::with_capacity(length as usize); + + let rc = unsafe { + securitybaseapi::GetFileSecurityW( + folder_name.as_ptr(), + OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + buf.as_mut_ptr() as *mut c_void, + length, + &mut length, + ) + }; + + if rc != 1 { + return Err("GetFileSecurityW failed to retrieve the security descriptor"); + } + + let mut token: HANDLE = 0 as HANDLE; + let rc = unsafe { + processthreadsapi::OpenProcessToken( + processthreadsapi::GetCurrentProcess(), + TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ, + &mut token, + ) + }; + if rc != 1 { + return Err("OpenProcessToken failed to retrieve current process' security token"); + } + + let mut impersonated_token: HANDLE = 0 as HANDLE; + let rc = unsafe { + securitybaseapi::DuplicateToken(token, SecurityImpersonation, &mut impersonated_token) + }; + if rc != 1 { + unsafe { handleapi::CloseHandle(token) }; + return Err("DuplicateToken failed"); + } + + let mut mapping: GENERIC_MAPPING = GENERIC_MAPPING { + GenericRead: FILE_GENERIC_READ, + GenericWrite: FILE_GENERIC_WRITE, + GenericExecute: FILE_GENERIC_EXECUTE, + GenericAll: FILE_ALL_ACCESS, + }; + + let mut priviledges: PRIVILEGE_SET = PRIVILEGE_SET::default(); + let mut priv_size = mem::size_of::() as DWORD; + let mut granted_access: DWORD = 0; + let mut access_rights: DWORD = FILE_GENERIC_WRITE; + let mut result: BOOL = 0 as BOOL; + unsafe { securitybaseapi::MapGenericMask(&mut access_rights, &mut mapping) }; + let rc = unsafe { + securitybaseapi::AccessCheck( + buf.as_mut_ptr() as PSECURITY_DESCRIPTOR, + impersonated_token, + access_rights, + &mut mapping, + &mut priviledges, + &mut priv_size, + &mut granted_access, + &mut result, + ) + }; + unsafe { + handleapi::CloseHandle(impersonated_token); + handleapi::CloseHandle(token); + } + + if rc != 1 { + return Err("AccessCheck failed"); + } + + Ok(result != 0) +} diff --git a/src/modules/utils/mod.rs b/src/modules/utils/mod.rs index 345ce7f4..d164de4d 100644 --- a/src/modules/utils/mod.rs +++ b/src/modules/utils/mod.rs @@ -1,5 +1,11 @@ pub mod directory; pub mod java_version_parser; +#[cfg(target_os = "windows")] +pub mod directory_win; + +#[cfg(not(target_os = "windows"))] +pub mod directory_nix; + #[cfg(test)] pub mod test; diff --git a/tests/testsuite/directory.rs b/tests/testsuite/directory.rs index 8f3ee515..70327642 100755 --- a/tests/testsuite/directory.rs +++ b/tests/testsuite/directory.rs @@ -138,7 +138,14 @@ fn root_directory() -> io::Result<()> { .output()?; let actual = String::from_utf8(output.stdout).unwrap(); - let expected = format!("{} ", Color::Cyan.bold().paint("/")); + #[cfg(not(target_os = "windows"))] + let expected = format!( + "{}{} ", + Color::Cyan.bold().paint("/"), + Color::Red.normal().paint("🔒") + ); + #[cfg(target_os = "windows")] + let expected = format!("{} ", Color::Cyan.bold().paint("/"),); assert_eq!(expected, actual); Ok(()) } @@ -151,7 +158,11 @@ fn directory_in_root() -> io::Result<()> { .output()?; let actual = String::from_utf8(output.stdout).unwrap(); - let expected = format!("{} ", Color::Cyan.bold().paint("/etc")); + let expected = format!( + "{}{} ", + Color::Cyan.bold().paint("/etc"), + Color::Red.normal().paint("🔒") + ); assert_eq!(expected, actual); Ok(()) }