feat(directory): Show lock symbol if current directory is read only (#1298)

Add feature to display icon if current directory is read-only.
This commit is contained in:
Alexey Chernyshov 2020-07-14 00:55:42 +03:00 committed by GitHub
parent 0db640396b
commit 57c39437bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 260 additions and 11 deletions

17
Cargo.lock generated
View File

@ -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",
]

View File

@ -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

View File

@ -589,13 +589,15 @@ it would have been `nixpkgs/pkgs`.
### Options
| Option | Default | Description |
| ------------------- | -------------------- | -------------------------------------------------------------------------------- |
| 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) "` | The format for the module. |
| `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. |
<details>
<summary>This module has a few advanced configuration options that control how the directory is displayed.</summary>

View File

@ -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",
}
}
}

View File

@ -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<Module<'a>> {
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<Module<'a>> {
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

View File

@ -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<bool, &'static str> {
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<u32> {
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<u32> {
// 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")
);
}
}

View File

@ -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<bool, &'static str> {
let folder_name: Vec<u16> = 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<u8> = 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::<PRIVILEGE_SET>() 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)
}

View File

@ -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;

View File

@ -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(())
}