cargo/util/context/
path.rs

1use super::{GlobalContext, StringList, Value};
2use regex::Regex;
3use serde::{de::Error, Deserialize};
4use std::path::PathBuf;
5
6/// Use with the `get` API to fetch a string that will be converted to a
7/// `PathBuf`. Relative paths are converted to absolute paths based on the
8/// location of the config file.
9#[derive(Debug, Deserialize, PartialEq, Clone)]
10#[serde(transparent)]
11pub struct ConfigRelativePath(Value<String>);
12
13impl ConfigRelativePath {
14    pub fn new(path: Value<String>) -> ConfigRelativePath {
15        ConfigRelativePath(path)
16    }
17
18    /// Returns the underlying value.
19    pub fn value(&self) -> &Value<String> {
20        &self.0
21    }
22
23    /// Returns the raw underlying configuration value for this key.
24    pub fn raw_value(&self) -> &str {
25        &self.0.val
26    }
27
28    /// Resolves this configuration-relative path to an absolute path.
29    ///
30    /// This will always return an absolute path where it's relative to the
31    /// location for configuration for this value.
32    pub fn resolve_path(&self, gctx: &GlobalContext) -> PathBuf {
33        self.0.definition.root(gctx).join(&self.0.val)
34    }
35
36    /// Same as [`Self::resolve_path`] but will make string replacements
37    /// before resolving the path.
38    ///
39    /// `replacements` should be an [`IntoIterator`] of tuples with the "from" and "to" for the
40    /// string replacement
41    pub fn resolve_templated_path(
42        &self,
43        gctx: &GlobalContext,
44        replacements: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
45    ) -> Result<PathBuf, ResolveTemplateError> {
46        let mut value = self.0.val.clone();
47
48        for (from, to) in replacements {
49            value = value.replace(from.as_ref(), to.as_ref());
50        }
51
52        // Check for expected variables
53        let re = Regex::new(r"\{(.*)\}").unwrap();
54        if let Some(caps) = re.captures(&value) {
55            return Err(ResolveTemplateError::UnexpectedVariable {
56                variable: caps[1].to_string(),
57                raw_template: self.0.val.clone(),
58            });
59        };
60
61        if value.contains("{") {
62            return Err(ResolveTemplateError::UnexpectedBracket {
63                bracket_type: BracketType::Opening,
64                raw_template: self.0.val.clone(),
65            });
66        }
67
68        if value.contains("}") {
69            return Err(ResolveTemplateError::UnexpectedBracket {
70                bracket_type: BracketType::Closing,
71                raw_template: self.0.val.clone(),
72            });
73        }
74
75        Ok(self.0.definition.root(gctx).join(&value))
76    }
77
78    /// Resolves this configuration-relative path to either an absolute path or
79    /// something appropriate to execute from `PATH`.
80    ///
81    /// Values which don't look like a filesystem path (don't contain `/` or
82    /// `\`) will be returned as-is, and everything else will fall through to an
83    /// absolute path.
84    pub fn resolve_program(&self, gctx: &GlobalContext) -> PathBuf {
85        gctx.string_to_path(&self.0.val, &self.0.definition)
86    }
87}
88
89/// A config type that is a program to run.
90///
91/// This supports a list of strings like `['/path/to/program', 'somearg']`
92/// or a space separated string like `'/path/to/program somearg'`.
93///
94/// This expects the first value to be the path to the program to run.
95/// Subsequent values are strings of arguments to pass to the program.
96///
97/// Typically you should use `ConfigRelativePath::resolve_program` on the path
98/// to get the actual program.
99///
100/// **Note**: Any usage of this type in config needs to be listed in
101/// the `util::context::is_nonmergable_list` check to prevent list merging
102/// from multiple config files.
103#[derive(Debug, Clone, PartialEq)]
104pub struct PathAndArgs {
105    pub path: ConfigRelativePath,
106    pub args: Vec<String>,
107}
108
109impl<'de> serde::Deserialize<'de> for PathAndArgs {
110    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111    where
112        D: serde::Deserializer<'de>,
113    {
114        let vsl = Value::<StringList>::deserialize(deserializer)?;
115        let mut strings = vsl.val.0;
116        if strings.is_empty() {
117            return Err(D::Error::invalid_length(0, &"at least one element"));
118        }
119        let first = strings.remove(0);
120        let crp = Value {
121            val: first,
122            definition: vsl.definition,
123        };
124        Ok(PathAndArgs {
125            path: ConfigRelativePath(crp),
126            args: strings,
127        })
128    }
129}
130
131impl PathAndArgs {
132    /// Construct a `PathAndArgs` from a string. The string will be split on ascii whitespace,
133    /// with the first item being treated as a `ConfigRelativePath` to the executable, and subsequent
134    /// items as arguments.
135    pub fn from_whitespace_separated_string(p: &Value<String>) -> PathAndArgs {
136        let mut iter = p.val.split_ascii_whitespace().map(str::to_string);
137        let val = iter.next().unwrap_or_default();
138        let args = iter.collect();
139        let crp = Value {
140            val,
141            definition: p.definition.clone(),
142        };
143        PathAndArgs {
144            path: ConfigRelativePath(crp),
145            args,
146        }
147    }
148}
149
150#[derive(Debug)]
151pub enum ResolveTemplateError {
152    UnexpectedVariable {
153        variable: String,
154        raw_template: String,
155    },
156    UnexpectedBracket {
157        bracket_type: BracketType,
158        raw_template: String,
159    },
160}
161
162#[derive(Debug)]
163pub enum BracketType {
164    Opening,
165    Closing,
166}