cargo/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 /// Creates a `ConfigKey` from the `key` specified.
49 ///
50 /// The `key` specified is expected to be a period-separated toml
51 /// configuration key.
52 pub fn from_parts(parts: impl IntoIterator<Item = impl AsRef<str>>) -> ConfigKey {
53 let mut cfg = ConfigKey::new();
54 for part in parts {
55 cfg.push(part.as_ref());
56 }
57 cfg
58 }
59
60 /// Pushes a new sub-key on this `ConfigKey`. This sub-key should be
61 /// equivalent to accessing a sub-table in TOML.
62 ///
63 /// Note that this considers `name` to be case-insensitive, meaning that the
64 /// corresponding toml key is appended with this `name` as-is and the
65 /// corresponding env key is appended with `name` after transforming it to
66 /// uppercase characters.
67 pub fn push(&mut self, name: &str) {
68 let env = name.replace(['-', '.'], "_").to_uppercase();
69 self._push(&env, name);
70 }
71
72 /// Performs the same function as `push` except that the corresponding
73 /// environment variable does not get the uppercase letters of `name` but
74 /// instead `name` is pushed raw onto the corresponding environment
75 /// variable.
76 pub fn push_sensitive(&mut self, name: &str) {
77 self._push(name, name);
78 }
79
80 fn _push(&mut self, env: &str, config: &str) {
81 self.parts.push((config.to_string(), self.env.len()));
82 self.env.push('_');
83 self.env.push_str(env);
84 }
85
86 /// Rewinds this `ConfigKey` back to the state it was at before the last
87 /// `push` method being called.
88 pub fn pop(&mut self) {
89 let (_part, env) = self.parts.pop().unwrap();
90 self.env.truncate(env);
91 }
92
93 /// Returns the corresponding environment variable key for this
94 /// configuration value.
95 pub fn as_env_key(&self) -> &str {
96 &self.env
97 }
98
99 /// Returns an iterator of the key parts as strings.
100 pub(crate) fn parts(&self) -> impl Iterator<Item = &str> {
101 self.parts.iter().map(|p| p.0.as_ref())
102 }
103
104 /// Returns whether or not this is a key for the root table.
105 pub fn is_root(&self) -> bool {
106 self.parts.is_empty()
107 }
108
109 /// Returns whether or not the given key string matches this key.
110 /// Use * to match any key part.
111 pub fn matches(&self, pattern: &str) -> bool {
112 let mut parts = self.parts();
113 pattern
114 .split('.')
115 .all(|pat| parts.next() == Some(pat) || pat == "*")
116 && parts.next().is_none()
117 }
118}
119
120impl fmt::Display for ConfigKey {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 let parts: Vec<_> = self.parts().map(|part| escape_key_part(part)).collect();
123 parts.join(".").fmt(f)
124 }
125}
126
127#[derive(Debug, Clone)]
128pub enum KeyOrIdx {
129 Key(String),
130 Idx(usize),
131}
132
133/// Tracks the key path to an item in an array for detailed error context.
134#[derive(Debug, Clone)]
135pub struct ArrayItemKeyPath {
136 base: ConfigKey,
137 /// This is stored in reverse, and is pushed only when erroring out.
138 /// The first element is the innermost key.
139 path: Vec<KeyOrIdx>,
140}
141
142impl ArrayItemKeyPath {
143 pub fn new(base: ConfigKey) -> Self {
144 Self {
145 base,
146 path: Vec::new(),
147 }
148 }
149
150 pub fn push_key(&mut self, k: String) {
151 self.path.push(KeyOrIdx::Key(k))
152 }
153
154 pub fn push_index(&mut self, i: usize) {
155 self.path.push(KeyOrIdx::Idx(i))
156 }
157}
158
159impl fmt::Display for ArrayItemKeyPath {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(f, "{}", self.base)?;
162 for k in self.path.iter().rev() {
163 match k {
164 KeyOrIdx::Key(s) => {
165 f.write_str(".")?;
166 f.write_str(&escape_key_part(&s))?;
167 }
168 KeyOrIdx::Idx(i) => {
169 write!(f, "[{i}]")?;
170 }
171 }
172 }
173 Ok(())
174 }
175}
176
177pub(super) fn escape_key_part<'a>(part: &'a str) -> Cow<'a, str> {
178 let ok = part.chars().all(|c| {
179 matches!(c,
180 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')
181 });
182 if ok {
183 Cow::Borrowed(part)
184 } else {
185 // This is a bit messy, but toml doesn't expose a function to do this.
186 Cow::Owned(toml::Value::from(part).to_string())
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use snapbox::assert_data_eq;
193 use snapbox::str;
194
195 use super::*;
196
197 #[test]
198 fn array_item_key_path_display() {
199 let base = ConfigKey::from_str("array");
200 let mut key_path = ArrayItemKeyPath::new(base);
201 // These are pushed in reverse order.
202 key_path.push_index(1);
203 key_path.push_key("rustflags".into());
204 key_path.push_key("thumbv8m.base-none-eabi".into());
205 key_path.push_index(2);
206 key_path.push_index(3);
207
208 assert_data_eq!(
209 key_path.to_string(),
210 str![[r#"array[3][2]."thumbv8m.base-none-eabi".rustflags[1]"#]]
211 );
212 }
213}