mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-04-19 23:31:25 +02:00
199 lines
6.6 KiB
Rust
199 lines
6.6 KiB
Rust
use std::collections::HashMap;
|
|
use std::ffi::OsStr;
|
|
use std::path::Component;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
/// Path extension trait
|
|
pub trait PathExt {
|
|
/// Resolve environment variable components in a path.
|
|
///
|
|
/// Resolves the following formats:
|
|
/// - CMD: `%variable%`
|
|
/// - PowerShell: `$Env:variable`
|
|
/// - Bash: `$variable`.
|
|
fn replace_env(&self) -> PathBuf;
|
|
}
|
|
|
|
/// Blanket implementation for all types that can be converted to a `Path`.
|
|
impl<P: AsRef<Path>> PathExt for P {
|
|
fn replace_env(&self) -> PathBuf {
|
|
let mut out = PathBuf::new();
|
|
|
|
for c in self.as_ref().components() {
|
|
match c {
|
|
Component::Normal(mut c) => {
|
|
// Special case for ~ and $HOME, replace with $Env:USERPROFILE
|
|
if c == OsStr::new("~") || c.eq_ignore_ascii_case("$HOME") {
|
|
c = OsStr::new("$Env:USERPROFILE");
|
|
}
|
|
|
|
let bytes = c.as_encoded_bytes();
|
|
|
|
// %LOCALAPPDATA%
|
|
let var = if bytes[0] == b'%' && bytes[bytes.len() - 1] == b'%' {
|
|
Some(&bytes[1..bytes.len() - 1])
|
|
} else {
|
|
// prefix length is 5 for $Env: and 1 for $
|
|
// so we take the minimum of 5 and the length of the bytes
|
|
let prefix = &bytes[..5.min(bytes.len())];
|
|
let prefix = unsafe { OsStr::from_encoded_bytes_unchecked(prefix) };
|
|
|
|
// $Env:LOCALAPPDATA
|
|
if prefix.eq_ignore_ascii_case("$Env:") {
|
|
Some(&bytes[5..])
|
|
} else if bytes[0] == b'$' {
|
|
// $LOCALAPPDATA
|
|
Some(&bytes[1..])
|
|
} else {
|
|
// not a variable
|
|
None
|
|
}
|
|
};
|
|
|
|
// if component is a variable, get the value from the environment
|
|
if let Some(var) = var {
|
|
let var = unsafe { OsStr::from_encoded_bytes_unchecked(var) };
|
|
if let Some(value) = std::env::var_os(var) {
|
|
out.push(value);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// if not a variable, or a value couldn't be obtained from environemnt
|
|
// then push the component as is
|
|
out.push(c);
|
|
}
|
|
|
|
// other components are pushed as is
|
|
_ => out.push(c),
|
|
}
|
|
}
|
|
|
|
out
|
|
}
|
|
}
|
|
|
|
/// Replace environment variables in a path. This is a wrapper around
|
|
/// [`PathExt::replace_env`] to be used in Clap arguments parsing.
|
|
pub fn replace_env_in_path(input: &str) -> Result<PathBuf, std::convert::Infallible> {
|
|
Ok(input.replace_env())
|
|
}
|
|
|
|
/// A wrapper around [`PathBuf`] that has a custom [Deserialize] implementation
|
|
/// that uses [`PathExt::replace_env`] to resolve environment variables
|
|
#[derive(Clone, Debug)]
|
|
pub struct ResolvedPathBuf(PathBuf);
|
|
|
|
impl ResolvedPathBuf {
|
|
/// Create a new [`ResolvedPathBuf`] from a [`PathBuf`]
|
|
pub fn new(path: PathBuf) -> Self {
|
|
Self(path.replace_env())
|
|
}
|
|
}
|
|
|
|
impl From<ResolvedPathBuf> for PathBuf {
|
|
fn from(path: ResolvedPathBuf) -> Self {
|
|
path.0
|
|
}
|
|
}
|
|
|
|
impl serde_with::SerializeAs<PathBuf> for ResolvedPathBuf {
|
|
fn serialize_as<S>(path: &PathBuf, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
path.serialize(serializer)
|
|
}
|
|
}
|
|
|
|
impl<'de> serde_with::DeserializeAs<'de, PathBuf> for ResolvedPathBuf {
|
|
fn deserialize_as<D>(deserializer: D) -> Result<PathBuf, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let path = PathBuf::deserialize(deserializer)?;
|
|
Ok(path.replace_env())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "schemars")]
|
|
impl serde_with::schemars_1::JsonSchemaAs<PathBuf> for ResolvedPathBuf {
|
|
fn schema_name() -> std::borrow::Cow<'static, str> {
|
|
std::borrow::Cow::Borrowed("PathBuf")
|
|
}
|
|
|
|
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
|
schemars::json_schema!({
|
|
"type": "string",
|
|
"description": "A file system path. Environment variables like %VAR%, $Env:VAR, or $VAR are automatically resolved."
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Custom deserializer for [`Option<HashMap<usize, PathBuf>>`] that uses
|
|
/// [`PathExt::replace_env`] to resolve environment variables in the paths.
|
|
///
|
|
/// This is used in `WorkspaceConfig` struct because we can't use
|
|
/// #[serde_with::serde_as] as it doesn't handle [`Option<HashMap<usize, ResolvedPathBuf>>`]
|
|
/// quite well and generated compiler errors that can't be fixed because of Rust's orphan rule.
|
|
pub fn resolve_option_hashmap_usize_path<'de, D>(
|
|
deserializer: D,
|
|
) -> Result<Option<HashMap<usize, PathBuf>>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
let map = Option::<HashMap<usize, PathBuf>>::deserialize(deserializer)?;
|
|
Ok(map.map(|map| map.into_iter().map(|(k, v)| (k, v.replace_env())).collect()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// helper functions
|
|
fn expected<P: AsRef<Path>>(p: P) -> PathBuf {
|
|
// Ensure that the path is using the correct path separator for the OS.
|
|
p.as_ref().components().collect::<PathBuf>()
|
|
}
|
|
|
|
fn resolve<P: AsRef<Path>>(p: P) -> PathBuf {
|
|
p.replace_env()
|
|
}
|
|
|
|
#[test]
|
|
fn resolves_env_vars() {
|
|
// Set a variable for testing
|
|
unsafe {
|
|
std::env::set_var("VAR", "VALUE");
|
|
}
|
|
|
|
// %VAR% format
|
|
assert_eq!(resolve("/path/%VAR%/d"), expected("/path/VALUE/d"));
|
|
// $env:VAR format
|
|
assert_eq!(resolve("/path/$env:VAR/d"), expected("/path/VALUE/d"));
|
|
// $VAR format
|
|
assert_eq!(resolve("/path/$VAR/d"), expected("/path/VALUE/d"));
|
|
|
|
// non-existent variable
|
|
assert_eq!(resolve("/path/%ASD%/to/d"), expected("/path/%ASD%/to/d"));
|
|
assert_eq!(
|
|
resolve("/path/$env:ASD/to/d"),
|
|
expected("/path/$env:ASD/to/d")
|
|
);
|
|
assert_eq!(resolve("/path/$ASD/to/d"), expected("/path/$ASD/to/d"));
|
|
|
|
// Set a $env:USERPROFILE variable for testing
|
|
unsafe {
|
|
std::env::set_var("USERPROFILE", "C:\\Users\\user");
|
|
}
|
|
|
|
// ~ and $HOME should be replaced with $Env:USERPROFILE
|
|
assert_eq!(resolve("~"), expected("C:\\Users\\user"));
|
|
assert_eq!(resolve("$HOME"), expected("C:\\Users\\user"));
|
|
}
|
|
}
|