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:
parent
0db640396b
commit
57c39437bc
|
@ -88,7 +88,7 @@ dependencies = [
|
||||||
"lazycell",
|
"lazycell",
|
||||||
"libc",
|
"libc",
|
||||||
"mach",
|
"mach",
|
||||||
"nix",
|
"nix 0.15.0",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"uom",
|
"uom",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
@ -651,6 +651,19 @@ dependencies = [
|
||||||
"void",
|
"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]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "5.1.2"
|
version = "5.1.2"
|
||||||
|
@ -1152,6 +1165,7 @@ dependencies = [
|
||||||
"git2",
|
"git2",
|
||||||
"log",
|
"log",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
|
"nix 0.17.0",
|
||||||
"nom",
|
"nom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"open",
|
"open",
|
||||||
|
@ -1174,6 +1188,7 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"winapi",
|
||||||
"yaml-rust",
|
"yaml-rust",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,12 @@ quick-xml = "0.18.1"
|
||||||
attohttpc = { version = "0.15.0", optional = true, default-features = false, features = ["tls", "form"] }
|
attohttpc = { version = "0.15.0", optional = true, default-features = false, features = ["tls", "form"] }
|
||||||
native-tls = { version = "0.2", optional = true }
|
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]
|
[dev-dependencies]
|
||||||
tempfile = "3.1.0"
|
tempfile = "3.1.0"
|
||||||
# More realiable than std::fs version on Windows
|
# More realiable than std::fs version on Windows
|
||||||
|
|
|
@ -589,13 +589,15 @@ it would have been `nixpkgs/pkgs`.
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Variable | Default | Description |
|
||||||
| ------------------- | -------------------- | -------------------------------------------------------------------------------- |
|
| ------------------------ | ----------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||||
| `truncation_length` | `3` | The number of parent folders that the current directory should be truncated to. |
|
| `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. |
|
| `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. |
|
| `style` | `"bold cyan"` | The style for the module. |
|
||||||
| `disabled` | `false` | Disables the `directory` 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>
|
<details>
|
||||||
<summary>This module has a few advanced configuration options that control how the directory is displayed.</summary>
|
<summary>This module has a few advanced configuration options that control how the directory is displayed.</summary>
|
||||||
|
|
|
@ -13,6 +13,8 @@ pub struct DirectoryConfig<'a> {
|
||||||
pub format: &'a str,
|
pub format: &'a str,
|
||||||
pub style: &'a str,
|
pub style: &'a str,
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
pub read_only_symbol: &'a str,
|
||||||
|
pub read_only_symbol_style: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
|
impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
|
||||||
|
@ -23,9 +25,11 @@ impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
|
||||||
fish_style_pwd_dir_length: 0,
|
fish_style_pwd_dir_length: 0,
|
||||||
substitutions: HashMap::new(),
|
substitutions: HashMap::new(),
|
||||||
use_logical_path: true,
|
use_logical_path: true,
|
||||||
format: "[$path]($style) ",
|
format: "[$path]($style)[$read_only]($read_only_style) ",
|
||||||
style: "cyan bold",
|
style: "cyan bold",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
read_only_symbol: "🔒",
|
||||||
|
read_only_symbol_style: "red",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 path_slash::PathExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::iter::FromIterator;
|
use std::iter::FromIterator;
|
||||||
|
@ -91,15 +95,24 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
String::from("")
|
String::from("")
|
||||||
};
|
};
|
||||||
let final_dir_string = format!("{}{}", fish_prefix, truncated_dir_string);
|
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| {
|
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
|
||||||
formatter
|
formatter
|
||||||
.map_style(|variable| match variable {
|
.map_style(|variable| match variable {
|
||||||
"style" => Some(Ok(config.style)),
|
"style" => Some(Ok(config.style)),
|
||||||
|
"read_only_style" => Some(Ok(config.read_only_symbol_style)),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.map(|variable| match variable {
|
.map(|variable| match variable {
|
||||||
"path" => Some(Ok(&final_dir_string)),
|
"path" => Some(Ok(&final_dir_string)),
|
||||||
|
"read_only" => {
|
||||||
|
if is_readonly_dir(current_dir.to_str()?) {
|
||||||
|
Some(Ok(&lock_symbol))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.parse(None)
|
.parse(None)
|
||||||
|
@ -116,6 +129,20 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
||||||
Some(module)
|
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
|
/// Contract the root component of a path
|
||||||
///
|
///
|
||||||
/// Replaces the `top_level_path` in a given `full_path` with the provided
|
/// Replaces the `top_level_path` in a given `full_path` with the provided
|
||||||
|
|
|
@ -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")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -1,5 +1,11 @@
|
||||||
pub mod directory;
|
pub mod directory;
|
||||||
pub mod java_version_parser;
|
pub mod java_version_parser;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub mod directory_win;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub mod directory_nix;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
|
@ -138,7 +138,14 @@ fn root_directory() -> io::Result<()> {
|
||||||
.output()?;
|
.output()?;
|
||||||
let actual = String::from_utf8(output.stdout).unwrap();
|
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);
|
assert_eq!(expected, actual);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -151,7 +158,11 @@ fn directory_in_root() -> io::Result<()> {
|
||||||
.output()?;
|
.output()?;
|
||||||
let actual = String::from_utf8(output.stdout).unwrap();
|
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);
|
assert_eq!(expected, actual);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue