1
0
mirror of https://github.com/Llewellynvdm/starship.git synced 2024-12-11 21:57:52 +00:00
starship/src/modules/direnv.rs

534 lines
15 KiB
Rust
Raw Normal View History

use std::borrow::Cow;
use std::path::PathBuf;
use std::str::FromStr;
use super::{Context, Module, ModuleConfig};
use crate::configs::direnv::DirenvConfig;
use crate::formatter::StringFormatter;
use serde::Deserialize;
/// Creates a module with the current direnv rc
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
let mut module = context.new_module("direnv");
let config = DirenvConfig::try_load(module.config);
let direnv_applies = !config.disabled
&& context
.try_begin_scan()?
.set_extensions(&config.detect_extensions)
.set_files(&config.detect_files)
.set_folders(&config.detect_folders)
.is_match();
if !direnv_applies {
return None;
}
// the `--json` flag is silently ignored for direnv versions <2.33.0
let direnv_status = &context.exec_cmd("direnv", &["status", "--json"])?.stdout;
let state = match DirenvState::from_str(direnv_status) {
Ok(s) => s,
Err(e) => {
log::warn!("{e}");
return None;
}
};
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
formatter
.map_style(|variable| match variable {
"style" => Some(Ok(config.style)),
_ => None,
})
.map(|variable| match variable {
"symbol" => Some(Ok(Cow::from(config.symbol))),
"rc_path" => Some(Ok(state.rc_path.to_string_lossy())),
"allowed" => Some(Ok(match state.allowed {
AllowStatus::Allowed => Cow::from(config.allowed_msg),
AllowStatus::NotAllowed => Cow::from(config.not_allowed_msg),
AllowStatus::Denied => Cow::from(config.denied_msg),
})),
"loaded" => state
.loaded
.then_some(config.loaded_msg)
.or(Some(config.unloaded_msg))
.map(Cow::from)
.map(Ok),
_ => None,
})
.parse(None, Some(context))
});
module.set_segments(match parsed {
Ok(segments) => segments,
Err(e) => {
log::warn!("{e}");
return None;
}
});
Some(module)
}
struct DirenvState {
pub rc_path: PathBuf,
pub allowed: AllowStatus,
pub loaded: bool,
}
impl FromStr for DirenvState {
type Err = Cow<'static, str>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match serde_json::from_str::<RawDirenvState>(s) {
Ok(raw) => Ok(Self {
rc_path: raw.state.found_rc.path,
allowed: raw.state.found_rc.allowed.try_into()?,
loaded: matches!(
raw.state.loaded_rc.allowed.try_into()?,
AllowStatus::Allowed
),
}),
Err(_) => Self::from_lines(s),
}
}
}
impl DirenvState {
fn from_lines(s: &str) -> Result<Self, Cow<'static, str>> {
let mut rc_path = PathBuf::new();
let mut allowed = None;
let mut loaded = true;
for line in s.lines() {
if let Some(path) = line.strip_prefix("Found RC path") {
rc_path = PathBuf::from_str(path.trim()).map_err(|e| Cow::from(e.to_string()))?
} else if let Some(value) = line.strip_prefix("Found RC allowed") {
allowed = Some(AllowStatus::from_str(value.trim())?);
} else if line.contains("No .envrc or .env loaded") {
loaded = false;
};
}
if rc_path.as_os_str().is_empty() || allowed.is_none() {
return Err(Cow::from("unknown direnv state"));
}
Ok(Self {
rc_path,
allowed: allowed.unwrap(),
loaded,
})
}
}
#[derive(Debug)]
enum AllowStatus {
Allowed,
NotAllowed,
Denied,
}
impl FromStr for AllowStatus {
type Err = Cow<'static, str>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"0" | "true" => Ok(Self::Allowed),
"1" => Ok(Self::NotAllowed),
"2" | "false" => Ok(Self::Denied),
_ => Err(Cow::from("invalid allow status")),
}
}
}
impl TryFrom<u8> for AllowStatus {
type Error = Cow<'static, str>;
fn try_from(u: u8) -> Result<Self, Self::Error> {
match u {
0 => Ok(Self::Allowed),
1 => Ok(Self::NotAllowed),
2 => Ok(Self::Denied),
_ => Err(Cow::from("unknown integer allow status")),
}
}
}
#[derive(Debug, Deserialize)]
struct RawDirenvState {
pub state: State,
}
#[derive(Debug, Deserialize)]
struct State {
#[serde(rename = "foundRC")]
pub found_rc: RCStatus,
#[serde(rename = "loadedRC")]
pub loaded_rc: RCStatus,
}
#[derive(Debug, Deserialize)]
struct RCStatus {
pub allowed: u8,
pub path: PathBuf,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::test::ModuleRenderer;
use crate::utils::CommandOutput;
use std::io;
use std::path::Path;
#[test]
fn folder_without_rc_files_pre_2_33() {
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_without_rc(),
stderr: String::default(),
}),
);
assert_eq!(None, renderer.collect());
}
#[test]
fn folder_without_rc_files() {
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_without_rc_json(),
stderr: String::default(),
}),
);
assert_eq!(None, renderer.collect());
}
#[test]
fn folder_with_unloaded_rc_file_pre_2_33() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc(dir.path(), false, "0", true),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv not loaded/allowed ".to_string()),
renderer.collect()
);
dir.close()
}
#[test]
fn folder_with_unloaded_rc_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 1, 0),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv not loaded/allowed ".to_string()),
renderer.collect()
);
dir.close()
}
#[test]
fn folder_with_loaded_rc_file_pre_2_33() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc(dir.path(), true, "0", true),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv loaded/allowed ".to_string()),
renderer.collect()
);
dir.close()
}
#[test]
fn folder_with_loaded_rc_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 0),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv loaded/allowed ".to_string()),
renderer.collect()
);
dir.close()
}
#[test]
fn folder_with_loaded_and_denied_rc_file_pre_2_33() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc(dir.path(), true, "2", true),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv loaded/denied ".to_string()),
renderer.collect()
);
dir.close()
}
#[test]
fn folder_with_loaded_and_not_allowed_rc_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 1),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv loaded/not allowed ".to_string()),
renderer.collect()
);
dir.close()
}
#[test]
fn folder_with_loaded_and_denied_rc_file() -> io::Result<()> {
let dir = tempfile::tempdir()?;
let rc_path = dir.path().join(".envrc");
std::fs::File::create(rc_path)?.sync_all()?;
let renderer = ModuleRenderer::new("direnv")
.config(toml::toml! {
[direnv]
disabled = false
})
.path(dir.path())
.cmd(
"direnv status --json",
Some(CommandOutput {
stdout: status_cmd_output_with_rc_json(dir.path(), 0, 2),
stderr: String::default(),
}),
);
assert_eq!(
Some("direnv loaded/denied ".to_string()),
renderer.collect()
);
dir.close()
}
fn status_cmd_output_without_rc() -> String {
String::from(
r"\
direnv exec path /usr/bin/direnv
DIRENV_CONFIG /home/test/.config/direnv
bash_path /usr/bin/bash
disable_stdin false
warn_timeout 5s
whitelist.prefix []
whitelist.exact map[]
No .envrc or .env loaded
No .envrc or .env found",
)
}
fn status_cmd_output_without_rc_json() -> String {
json!({
"config": {
"ConfigDir": config_dir(),
"SelfPath": self_path(),
},
"state": {
"foundRC": null,
"loadedRC": null,
}
})
.to_string()
}
fn status_cmd_output_with_rc(
dir: impl AsRef<Path>,
loaded: bool,
allowed: &str,
use_legacy_boolean_flags: bool,
) -> String {
let rc_path = dir.as_ref().join(".envrc");
let rc_path = rc_path.to_string_lossy();
let allowed_value = match (use_legacy_boolean_flags, allowed) {
(true, "0") => "true",
(true, ..) => "false",
(false, val) => val,
};
let loaded = if loaded {
format!(
r#"\
Loaded RC path {rc_path}
Loaded watch: ".envrc" - 2023-04-30T09:51:04-04:00
Loaded watch: "../.local/share/direnv/allow/abcd" - 2023-04-30T09:52:58-04:00
Loaded RC allowed {allowed_value}
Loaded RC allowPath
"#
)
} else {
String::from("No .envrc or .env loaded")
};
let state = allowed.to_string();
format!(
r#"\
direnv exec path /usr/bin/direnv
DIRENV_CONFIG /home/test/.config/direnv
bash_path /usr/bin/bash
disable_stdin false
warn_timeout 5s
whitelist.prefix []
whitelist.exact map[]
{loaded}
Found RC path {rc_path}
Found watch: ".envrc" - 2023-04-25T18:45:54-04:00
Found watch: "../.local/share/direnv/allow/abcd" - 1969-12-31T19:00:00-05:00
Found RC allowed {state}
Found RC allowPath /home/test/.local/share/direnv/allow/abcd
"#
)
}
fn status_cmd_output_with_rc_json(dir: impl AsRef<Path>, loaded: u8, allowed: u8) -> String {
let rc_path = dir.as_ref().join(".envrc");
let rc_path = rc_path.to_string_lossy();
json!({
"config": {
"ConfigDir": config_dir(),
"SelfPath": self_path(),
},
"state": {
"foundRC": {
"allowed": allowed,
"path": rc_path,
},
"loadedRC": {
"allowed": loaded,
"path": rc_path,
}
}
})
.to_string()
}
#[cfg(windows)]
fn config_dir() -> &'static str {
r"C:\\Users\\test\\AppData\\Local\\direnv"
}
#[cfg(not(windows))]
fn config_dir() -> &'static str {
"/home/test/.config/direnv"
}
#[cfg(windows)]
fn self_path() -> &'static str {
r"C:\\Program Files\\direnv\\direnv.exe"
}
#[cfg(not(windows))]
fn self_path() -> &'static str {
"/usr/bin/direnv"
}
}