use crate::paths::{Error as PathError, SystemPath};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{env, fmt};
static ENV_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\$\{[^(=|\x{0}|$)]+?}").expect("failed to compile regular expression")
});
#[derive(Debug)]
pub enum Error {
Env {
error: env::VarError,
var: String,
},
Path(PathError),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Env {
error: error @ env::VarError::NotPresent,
var,
} => write!(f, "{error}: {var}"),
Self::Env {
error: error @ env::VarError::NotUnicode(_),
..
} => error.fmt(f),
Self::Path(error) => write!(f, "{error}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
Error::Env { error, .. } => Some(error),
Error::Path(error) => Some(error),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct StringWithEnv(String);
impl From<String> for StringWithEnv {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for StringWithEnv {
fn from(s: &str) -> Self {
Self::from(s.to_string())
}
}
impl fmt::Display for StringWithEnv {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl StringWithEnv {
pub fn process(self) -> Result<String, Error> {
let mut new_string = self.0;
let mut start: usize = 0;
let mut old_start: usize;
while let Some(mat) = ENV_REGEX.find(&new_string[start..]) {
let var = mat.as_str();
let var = &var[2..var.len() - 1];
tracing::trace!(var, "found environment variable {}", var,);
let value = env::var(var).map_err(|error| Error::Env {
error,
var: var.to_string(),
})?;
old_start = start;
start += mat.start() + value.len();
if start > (new_string.len() + value.len() - mat.as_str().len()) {
start = new_string.len();
}
let range = mat.range();
tracing::trace!(
var,
path = %new_string,
%value,
"expanding first instance of variable in path"
);
new_string.replace_range(range.start + old_start..range.end + old_start, &value);
if start >= new_string.len() {
break;
}
}
Ok(new_string)
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct PathWithEnv(String);
impl From<String> for PathWithEnv {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for PathWithEnv {
fn from(s: &str) -> Self {
Self::from(s.to_string())
}
}
impl fmt::Display for PathWithEnv {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl PathWithEnv {
#[tracing::instrument(level = "debug", name = "process_path_with_env")]
pub fn process(self) -> Result<SystemPath, Error> {
let new_path = StringWithEnv(self.0).process()?;
SystemPath::try_from(PathBuf::from(new_path).components().collect::<PathBuf>())
.map_err(Error::Path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as _;
macro_rules! test_env {
(name: $name:ident, input: $input:literal, env: $var:literal, value: $value:literal, expected: $expected:literal, require_var: $require_var:literal) => {
#[test]
fn $name() {
assert!((!$require_var || ($input).contains(&format!("${{{}}}", $var))), "input string {} doesn't contain variable {}", $input, $var);
let old_val = std::env::var_os($var);
std::env::set_var($var, $value);
let (input, expected): (String, SystemPath) = {
#[cfg(windows)]
let (i, e) = (format!("C:{}", $input), format!("C:{}", $expected));
#[cfg(unix)]
let (i, e) = (String::from($input), String::from($expected));
(i, SystemPath::try_from(PathBuf::from(e)).unwrap())
};
let result = PathWithEnv::from(input).process().unwrap();
assert_eq!(result, expected);
if let Some(val) = old_val {
std::env::set_var($var, val);
}
}
};
(name: $name:ident, input: $input:literal, env: $var:literal, value: $value:literal, expected: $expected:literal) => {
test_env!{ name: $name, input: $input, env: $var, value: $value, expected: $expected, require_var: true }
};
}
test_env! {
name: var_at_start_shorter_than_value,
input: "${TEST_HOME}/test/file",
env: "TEST_HOME",
value: "/home/testuser",
expected: "/home/testuser/test/file"
}
test_env! {
name: var_in_middle_shorter_than_value,
input: "/home/testuser/${TEST_PATH}/file",
env: "TEST_PATH",
value: "test/subdir/subberdir",
expected: "/home/testuser/test/subdir/subberdir/file"
}
test_env! {
name: var_at_end_shorter_than_value,
input: "/home/testuser/${TEST_PATH}",
env: "TEST_PATH",
value: "test/subdir/file",
expected: "/home/testuser/test/subdir/file"
}
test_env! {
name: var_at_start_same_length_as_value,
input: "${TEST_HOME}/test/file",
env: "TEST_HOME",
value: "/home/tester",
expected: "/home/tester/test/file"
}
test_env! {
name: var_in_middle_same_length_as_value,
input: "/home/testuser/${TEST_PATH}/file",
env: "TEST_PATH",
value: "/test/folder",
expected: "/home/testuser/test/folder/file"
}
test_env! {
name: var_at_end_same_length_as_value,
input: "/home/testuser/${TEST_PATH}",
env: "TEST_PATH",
value: "testing/file",
expected: "/home/testuser/testing/file"
}
test_env! {
name: var_at_start_longer_than_value,
input: "${TEST_HOME}/test/file",
env: "TEST_HOME",
value: "/home/test",
expected: "/home/test/test/file"
}
test_env! {
name: var_in_middle_longer_than_value,
input: "/home/testuser/${TEST_PATH}/file",
env: "TEST_PATH",
value: "test/dir",
expected: "/home/testuser/test/dir/file"
}
test_env! {
name: var_at_end_longer_than_value,
input: "/home/testuser/${TEST_PATH}",
env: "TEST_PATH",
value: "a/file",
expected: "/home/testuser/a/file"
}
test_env! {
name: path_without_var_stays_same,
input: "/path/without/variables",
env: "UNUSED",
value: "NOTHING",
expected: "/path/without/variables",
require_var: false
}
test_env! {
name: path_with_two_variables,
input: "/home/${TEST_USER}/somedir/${TEST_USER}/file",
env: "TEST_USER",
value: "testuser",
expected: "/home/testuser/somedir/testuser/file"
}
test_env! {
name: var_without_braces_not_expanded,
input: "/path/with/$INVALID/variable",
env: "INVALID",
value: "broken",
expected: "/path/with/$INVALID/variable",
require_var: false
}
test_env! {
name: var_windows_style_not_expanded,
input: "/path/with/%INVALID%/variable",
env: "INVALID",
value: "broken",
expected: "/path/with/%INVALID%/variable",
require_var: false
}
test_env! {
name: vars_not_recursively_expanded,
input: "/${TEST_HOME}",
env: "TEST_HOME",
value: "${HOME}",
expected: "/${HOME}"
}
test_env! {
name: var_inside_var,
input: "/test/${WRAPPING${TEST_VAR}VARIABLE}/test",
env: "TEST_VAR",
value: "_",
expected: "/test/${WRAPPING_VARIABLE}/test"
}
#[test]
fn test_error_traits() {
let env_error = env::var("DOESNOTEXIST").expect_err("variable should not exist");
let error = Error::Env {
error: env_error,
var: "DOESNOTEXIST".to_string(),
};
assert!(error.to_string().contains("DOESNOTEXIST"));
assert!(error.source().is_some());
}
}