cargo/util/context/
key.rs

1use std::borrow::Cow;
2use std::fmt;
3
4/// Key for a configuration variable.
5///
6/// This type represents a configuration variable that we're looking up in
7/// Cargo's configuration. This structure simultaneously keeps track of a
8/// corresponding environment variable name as well as a TOML config name. The
9/// intention here is that this is built up and torn down over time efficiently,
10/// avoiding clones and such as possible.
11#[derive(Debug, Clone)]
12pub struct ConfigKey {
13    // The current environment variable this configuration key maps to. This is
14    // updated with `push` methods and looks like `CARGO_FOO_BAR` for pushing
15    // `foo` and then `bar`.
16    env: String,
17    // This is used to keep track of how many sub-keys have been pushed on
18    // this `ConfigKey`. Each element of this vector is a new sub-key pushed
19    // onto this `ConfigKey`. Each element is a pair where the first item is
20    // the key part as a string, and the second item is an index into `env`.
21    // The `env` index is used on `pop` to truncate `env` to rewind back to
22    // the previous `ConfigKey` state before a `push`.
23    parts: Vec<(String, usize)>,
24}
25
26impl ConfigKey {
27    /// Creates a new blank configuration key which is ready to get built up by
28    /// using `push` and `push_sensitive`.
29    pub fn new() -> ConfigKey {
30        ConfigKey {
31            env: "CARGO".to_string(),
32            parts: Vec::new(),
33        }
34    }
35
36    /// Creates a `ConfigKey` from the `key` specified.
37    ///
38    /// The `key` specified is expected to be a period-separated toml
39    /// configuration key.
40    pub fn from_str(key: &str) -> ConfigKey {
41        let mut cfg = ConfigKey::new();
42        for part in key.split('.') {
43            cfg.push(part);
44        }
45        cfg
46    }
47
48    /// Pushes a new sub-key on this `ConfigKey`. This sub-key should be
49    /// equivalent to accessing a sub-table in TOML.
50    ///
51    /// Note that this considers `name` to be case-insensitive, meaning that the
52    /// corresponding toml key is appended with this `name` as-is and the
53    /// corresponding env key is appended with `name` after transforming it to
54    /// uppercase characters.
55    pub fn push(&mut self, name: &str) {
56        let env = name.replace("-", "_").to_uppercase();
57        self._push(&env, name);
58    }
59
60    /// Performs the same function as `push` except that the corresponding
61    /// environment variable does not get the uppercase letters of `name` but
62    /// instead `name` is pushed raw onto the corresponding environment
63    /// variable.
64    pub fn push_sensitive(&mut self, name: &str) {
65        self._push(name, name);
66    }
67
68    fn _push(&mut self, env: &str, config: &str) {
69        self.parts.push((config.to_string(), self.env.len()));
70        self.env.push('_');
71        self.env.push_str(env);
72    }
73
74    /// Rewinds this `ConfigKey` back to the state it was at before the last
75    /// `push` method being called.
76    pub fn pop(&mut self) {
77        let (_part, env) = self.parts.pop().unwrap();
78        self.env.truncate(env);
79    }
80
81    /// Returns the corresponding environment variable key for this
82    /// configuration value.
83    pub fn as_env_key(&self) -> &str {
84        &self.env
85    }
86
87    /// Returns an iterator of the key parts as strings.
88    pub(crate) fn parts(&self) -> impl Iterator<Item = &str> {
89        self.parts.iter().map(|p| p.0.as_ref())
90    }
91
92    /// Returns whether or not this is a key for the root table.
93    pub fn is_root(&self) -> bool {
94        self.parts.is_empty()
95    }
96
97    /// Returns whether or not the given key string matches this key.
98    /// Use * to match any key part.
99    pub fn matches(&self, pattern: &str) -> bool {
100        let mut parts = self.parts();
101        pattern
102            .split('.')
103            .all(|pat| parts.next() == Some(pat) || pat == "*")
104            && parts.next().is_none()
105    }
106}
107
108impl fmt::Display for ConfigKey {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let parts: Vec<_> = self.parts().map(|part| escape_key_part(part)).collect();
111        parts.join(".").fmt(f)
112    }
113}
114
115#[derive(Debug, Clone)]
116pub enum KeyOrIdx {
117    Key(String),
118    Idx(usize),
119}
120
121/// Tracks the key path to an item in an array for detailed errro context.
122#[derive(Debug, Clone)]
123pub struct ArrayItemKeyPath {
124    base: ConfigKey,
125    /// This is stored in reverse, and is pushed only when erroring out.
126    /// The first element is the innermost key.
127    path: Vec<KeyOrIdx>,
128}
129
130impl ArrayItemKeyPath {
131    pub fn new(base: ConfigKey) -> Self {
132        Self {
133            base,
134            path: Vec::new(),
135        }
136    }
137
138    pub fn push_key(&mut self, k: String) {
139        self.path.push(KeyOrIdx::Key(k))
140    }
141
142    pub fn push_index(&mut self, i: usize) {
143        self.path.push(KeyOrIdx::Idx(i))
144    }
145}
146
147impl fmt::Display for ArrayItemKeyPath {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}", self.base)?;
150        for k in self.path.iter().rev() {
151            match k {
152                KeyOrIdx::Key(s) => {
153                    f.write_str(".")?;
154                    f.write_str(&escape_key_part(&s))?;
155                }
156                KeyOrIdx::Idx(i) => {
157                    write!(f, "[{i}]")?;
158                }
159            }
160        }
161        Ok(())
162    }
163}
164
165pub(super) fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> {
166    let ok = part.chars().all(|c| {
167        matches!(c,
168        'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')
169    });
170    if ok {
171        Cow::Borrowed(part)
172    } else {
173        // This is a bit messy, but toml doesn't expose a function to do this.
174        Cow::Owned(toml::Value::from(part).to_string())
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use snapbox::assert_data_eq;
181    use snapbox::str;
182
183    use super::*;
184
185    #[test]
186    fn array_item_key_path_display() {
187        let base = ConfigKey::from_str("array");
188        let mut key_path = ArrayItemKeyPath::new(base);
189        // These are pushed in reverse order.
190        key_path.push_index(1);
191        key_path.push_key("rustflags".into());
192        key_path.push_key("thumbv8m.base-none-eabi".into());
193        key_path.push_index(2);
194        key_path.push_index(3);
195
196        assert_data_eq!(
197            key_path.to_string(),
198            str![[r#"array[3][2]."thumbv8m.base-none-eabi".rustflags[1]"#]]
199        );
200    }
201}