mirror of
https://github.com/starship/starship.git
synced 2024-11-25 17:54:08 +01:00
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
17
Cargo.lock
generated
17
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
@ -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. |
|
||||
|
||||
<details>
|
||||
<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 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
61
src/modules/utils/directory_nix.rs
Normal file
61
src/modules/utils/directory_nix.rs
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
117
src/modules/utils/directory_win.rs
Normal file
117
src/modules/utils/directory_win.rs
Normal 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)
|
||||
}
|
@ -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;
|
||||
|
@ -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(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user