binstalk_manifests/
cargo_config.rs

1//! Cargo's `.cargo/config.toml`
2//!
3//! This manifest is used by Cargo to load configurations stored by users.
4//!
5//! Binstall reads from them to be compatible with `cargo-install`'s behavior.
6
7use std::{
8    borrow::Cow,
9    collections::BTreeMap,
10    fs::File,
11    io,
12    path::{Path, PathBuf},
13};
14
15use compact_str::CompactString;
16use fs_lock::FileLock;
17use home::cargo_home;
18use miette::Diagnostic;
19use serde::Deserialize;
20use thiserror::Error;
21
22#[derive(Debug, Deserialize)]
23pub struct Install {
24    /// `cargo install` destination directory
25    pub root: Option<PathBuf>,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct Http {
30    /// HTTP proxy in libcurl format: "host:port"
31    ///
32    /// env: CARGO_HTTP_PROXY or HTTPS_PROXY or https_proxy or http_proxy
33    pub proxy: Option<CompactString>,
34    /// timeout for each HTTP request, in seconds
35    ///
36    /// env: CARGO_HTTP_TIMEOUT or HTTP_TIMEOUT
37    pub timeout: Option<u64>,
38    /// path to Certificate Authority (CA) bundle
39    pub cainfo: Option<PathBuf>,
40}
41
42#[derive(Eq, PartialEq, Debug, Deserialize)]
43#[serde(untagged)]
44pub enum Env {
45    Value(CompactString),
46    WithOptions {
47        value: CompactString,
48        force: Option<bool>,
49        relative: Option<bool>,
50    },
51}
52
53#[derive(Debug, Deserialize)]
54pub struct Registry {
55    pub index: Option<CompactString>,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct DefaultRegistry {
60    pub default: Option<CompactString>,
61}
62
63#[derive(Debug, Default, Deserialize)]
64pub struct Config {
65    pub install: Option<Install>,
66    pub http: Option<Http>,
67    pub env: Option<BTreeMap<CompactString, Env>>,
68    pub registries: Option<BTreeMap<CompactString, Registry>>,
69    pub registry: Option<DefaultRegistry>,
70}
71
72fn join_if_relative(path: Option<&mut PathBuf>, dir: &Path) {
73    match path {
74        Some(path) if path.is_relative() => *path = dir.join(&*path),
75        _ => (),
76    }
77}
78
79impl Config {
80    pub fn default_path() -> Result<PathBuf, ConfigLoadError> {
81        Ok(cargo_home()?.join("config.toml"))
82    }
83
84    pub fn load() -> Result<Self, ConfigLoadError> {
85        Self::load_from_path(Self::default_path()?)
86    }
87
88    /// * `dir` - path to the dir where the config.toml is located.
89    ///           For relative path in the config, `Config::load_from_reader`
90    ///           will join the `dir` and the relative path to form the final
91    ///           path.
92    pub fn load_from_reader<R: io::Read>(
93        mut reader: R,
94        dir: &Path,
95    ) -> Result<Self, ConfigLoadError> {
96        fn inner(reader: &mut dyn io::Read, dir: &Path) -> Result<Config, ConfigLoadError> {
97            let mut vec = Vec::new();
98            reader.read_to_end(&mut vec)?;
99
100            if vec.is_empty() {
101                Ok(Default::default())
102            } else {
103                let mut config: Config = toml_edit::de::from_slice(&vec)?;
104                join_if_relative(
105                    config
106                        .install
107                        .as_mut()
108                        .and_then(|install| install.root.as_mut()),
109                    dir,
110                );
111                join_if_relative(
112                    config.http.as_mut().and_then(|http| http.cainfo.as_mut()),
113                    dir,
114                );
115                if let Some(envs) = config.env.as_mut() {
116                    for env in envs.values_mut() {
117                        if let Env::WithOptions {
118                            value,
119                            relative: Some(true),
120                            ..
121                        } = env
122                        {
123                            let path = Cow::Borrowed(Path::new(&value));
124                            if path.is_relative() {
125                                *value = dir.join(&path).to_string_lossy().into();
126                            }
127                        }
128                    }
129                }
130                Ok(config)
131            }
132        }
133
134        inner(&mut reader, dir)
135    }
136
137    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, ConfigLoadError> {
138        fn inner(path: &Path) -> Result<Config, ConfigLoadError> {
139            match File::open(path) {
140                Ok(file) => {
141                    let file = FileLock::new_shared(file)?.set_file_path(path);
142                    // Any regular file must have a parent dir
143                    Config::load_from_reader(file, path.parent().unwrap())
144                }
145                Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Default::default()),
146                Err(err) => Err(err.into()),
147            }
148        }
149
150        inner(path.as_ref())
151    }
152}
153
154#[derive(Debug, Diagnostic, Error)]
155#[non_exhaustive]
156pub enum ConfigLoadError {
157    #[error("I/O Error: {0}")]
158    Io(#[from] io::Error),
159
160    #[error("Failed to deserialize toml: {0}")]
161    TomlParse(Box<toml_edit::de::Error>),
162}
163
164impl From<toml_edit::de::Error> for ConfigLoadError {
165    fn from(e: toml_edit::de::Error) -> Self {
166        ConfigLoadError::TomlParse(Box::new(e))
167    }
168}
169
170impl From<toml_edit::TomlError> for ConfigLoadError {
171    fn from(e: toml_edit::TomlError) -> Self {
172        ConfigLoadError::TomlParse(Box::new(e.into()))
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    use std::{io::Cursor, path::MAIN_SEPARATOR};
181
182    use compact_str::format_compact;
183
184    const CONFIG: &str = r#"
185[env]
186# Set ENV_VAR_NAME=value for any process run by Cargo
187ENV_VAR_NAME = "value"
188# Set even if already present in environment
189ENV_VAR_NAME_2 = { value = "value", force = true }
190# Value is relative to .cargo directory containing `config.toml`, make absolute
191ENV_VAR_NAME_3 = { value = "relative-path", relative = true }
192
193[http]
194debug = false               # HTTP debugging
195proxy = "host:port"         # HTTP proxy in libcurl format
196timeout = 30                # timeout for each HTTP request, in seconds
197cainfo = "cert.pem"         # path to Certificate Authority (CA) bundle
198
199[install]
200root = "/some/path"         # `cargo install` destination directory
201    "#;
202
203    #[test]
204    fn test_loading() {
205        let config = Config::load_from_reader(Cursor::new(&CONFIG), Path::new("root")).unwrap();
206
207        assert_eq!(
208            config.install.unwrap().root.as_deref().unwrap(),
209            Path::new("/some/path")
210        );
211
212        let http = config.http.unwrap();
213        assert_eq!(http.proxy.unwrap(), CompactString::const_new("host:port"));
214        assert_eq!(http.timeout.unwrap(), 30);
215        assert_eq!(http.cainfo.unwrap(), Path::new("root").join("cert.pem"));
216
217        let env = config.env.unwrap();
218        assert_eq!(env.len(), 3);
219        assert_eq!(
220            env.get("ENV_VAR_NAME").unwrap(),
221            &Env::Value(CompactString::const_new("value"))
222        );
223        assert_eq!(
224            env.get("ENV_VAR_NAME_2").unwrap(),
225            &Env::WithOptions {
226                value: CompactString::new("value"),
227                force: Some(true),
228                relative: None,
229            }
230        );
231        assert_eq!(
232            env.get("ENV_VAR_NAME_3").unwrap(),
233            &Env::WithOptions {
234                value: format_compact!("root{MAIN_SEPARATOR}relative-path"),
235                force: None,
236                relative: Some(true),
237            }
238        );
239    }
240}