1 //! Tools for parsing [Cargo configuration](https://doc.rust-lang.org/cargo/reference/config.html) files
2
3 use std::collections::BTreeMap;
4 use std::fs;
5 use std::path::Path;
6 use std::str::FromStr;
7
8 use crate::utils;
9 use anyhow::{bail, Result};
10 use serde::{Deserialize, Serialize};
11
12 /// The [`[registry]`](https://doc.rust-lang.org/cargo/reference/config.html#registry)
13 /// table controls the default registry used when one is not specified.
14 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
15 pub(crate) struct Registry {
16 /// name of the default registry
17 pub(crate) default: String,
18
19 /// authentication token for crates.io
20 pub(crate) token: Option<String>,
21 }
22
23 /// The [`[source]`](https://doc.rust-lang.org/cargo/reference/config.html#source)
24 /// table defines the registry sources available.
25 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
26 pub(crate) struct Source {
27 /// replace this source with the given named source
28 #[serde(rename = "replace-with")]
29 pub(crate) replace_with: Option<String>,
30
31 /// URL to a registry source
32 #[serde(default = "default_registry_url")]
33 pub(crate) registry: String,
34 }
35
36 /// This is the default registry url per what's defined by Cargo.
default_registry_url() -> String37 fn default_registry_url() -> String {
38 utils::CRATES_IO_INDEX_URL.to_owned()
39 }
40
41 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
42 /// registries other than crates.io
43 pub(crate) struct AdditionalRegistry {
44 /// URL of the registry index
45 pub(crate) index: String,
46
47 /// authentication token for the registry
48 pub(crate) token: Option<String>,
49 }
50
51 /// A subset of a Cargo configuration file. The schema here is only what
52 /// is required for parsing registry information.
53 /// See [cargo docs](https://doc.rust-lang.org/cargo/reference/config.html#configuration-format)
54 /// for more details.
55 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
56 pub(crate) struct CargoConfig {
57 /// registries other than crates.io
58 #[serde(default = "default_registries")]
59 pub(crate) registries: BTreeMap<String, AdditionalRegistry>,
60
61 #[serde(default = "default_registry")]
62 pub(crate) registry: Registry,
63
64 /// source definition and replacement
65 #[serde(default = "BTreeMap::new")]
66 pub(crate) source: BTreeMap<String, Source>,
67 }
68
69 /// Each Cargo config is expected to have a default `crates-io` registry.
default_registries() -> BTreeMap<String, AdditionalRegistry>70 fn default_registries() -> BTreeMap<String, AdditionalRegistry> {
71 let mut registries = BTreeMap::new();
72 registries.insert(
73 "crates-io".to_owned(),
74 AdditionalRegistry {
75 index: default_registry_url(),
76 token: None,
77 },
78 );
79 registries
80 }
81
82 /// Each Cargo config has a default registry for `crates.io`.
default_registry() -> Registry83 fn default_registry() -> Registry {
84 Registry {
85 default: "crates-io".to_owned(),
86 token: None,
87 }
88 }
89
90 impl Default for CargoConfig {
default() -> Self91 fn default() -> Self {
92 let registries = default_registries();
93 let registry = default_registry();
94 let source = Default::default();
95
96 Self {
97 registries,
98 registry,
99 source,
100 }
101 }
102 }
103
104 impl FromStr for CargoConfig {
105 type Err = anyhow::Error;
106
from_str(s: &str) -> Result<Self, Self::Err>107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 let incoming: CargoConfig = toml::from_str(s)?;
109 let mut config = Self::default();
110 config.registries.extend(incoming.registries);
111 config.source.extend(incoming.source);
112 config.registry = incoming.registry;
113 Ok(config)
114 }
115 }
116
117 impl CargoConfig {
118 /// Load a Cargo config from a path to a file on disk.
try_from_path(path: &Path) -> Result<Self>119 pub(crate) fn try_from_path(path: &Path) -> Result<Self> {
120 let content = fs::read_to_string(path)?;
121 Self::from_str(&content)
122 }
123
124 /// Look up a registry [Source] by its url.
get_source_from_url(&self, url: &str) -> Option<&Source>125 pub(crate) fn get_source_from_url(&self, url: &str) -> Option<&Source> {
126 if let Some(found) = self.source.values().find(|v| v.registry == url) {
127 Some(found)
128 } else if url == utils::CRATES_IO_INDEX_URL {
129 self.source.get("crates-io")
130 } else {
131 None
132 }
133 }
134
get_registry_index_url_by_name(&self, name: &str) -> Option<&str>135 pub(crate) fn get_registry_index_url_by_name(&self, name: &str) -> Option<&str> {
136 if let Some(registry) = self.registries.get(name) {
137 Some(®istry.index)
138 } else if let Some(source) = self.source.get(name) {
139 Some(&source.registry)
140 } else {
141 None
142 }
143 }
144
resolve_replacement_url<'a>(&'a self, url: &'a str) -> Result<&'a str>145 pub(crate) fn resolve_replacement_url<'a>(&'a self, url: &'a str) -> Result<&'a str> {
146 if let Some(source) = self.get_source_from_url(url) {
147 if let Some(replace_with) = &source.replace_with {
148 if let Some(replacement) = self.get_registry_index_url_by_name(replace_with) {
149 Ok(replacement)
150 } else {
151 bail!("Tried to replace registry {} with registry named {} but didn't have metadata about the replacement", url, replace_with);
152 }
153 } else {
154 Ok(url)
155 }
156 } else {
157 Ok(url)
158 }
159 }
160 }
161
162 #[cfg(test)]
163 mod test {
164 use super::*;
165
166 use std::fs;
167
168 #[test]
registry_settings()169 fn registry_settings() {
170 let temp_dir = tempfile::tempdir().unwrap();
171 let config = temp_dir.as_ref().join("config.toml");
172
173 fs::write(&config, textwrap::dedent(
174 r#"
175 # Makes artifactory the default registry and saves passing --registry parameter
176 [registry]
177 default = "art-crates-remote"
178
179 [registries]
180 # Remote repository proxy in Artifactory (read-only)
181 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
182
183 # Optional, use with --registry to publish to crates.io
184 crates-io = { index = "https://github.com/rust-lang/crates.io-index" }
185
186 [net]
187 git-fetch-with-cli = true
188 "#,
189 )).unwrap();
190
191 let config = CargoConfig::try_from_path(&config).unwrap();
192 assert_eq!(
193 config,
194 CargoConfig {
195 registries: BTreeMap::from([
196 (
197 "art-crates-remote".to_owned(),
198 AdditionalRegistry {
199 index: "https://artprod.mycompany/artifactory/git/cargo-remote.git"
200 .to_owned(),
201 token: None,
202 },
203 ),
204 (
205 "crates-io".to_owned(),
206 AdditionalRegistry {
207 index: "https://github.com/rust-lang/crates.io-index".to_owned(),
208 token: None,
209 },
210 ),
211 ]),
212 registry: Registry {
213 default: "art-crates-remote".to_owned(),
214 token: None,
215 },
216 source: BTreeMap::new(),
217 },
218 )
219 }
220
221 #[test]
registry_settings_get_index_url_by_name_from_source()222 fn registry_settings_get_index_url_by_name_from_source() {
223 let temp_dir = tempfile::tempdir().unwrap();
224 let config = temp_dir.as_ref().join("config.toml");
225
226 fs::write(&config, textwrap::dedent(
227 r#"
228 [registries]
229 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
230
231 [source.crates-io]
232 replace-with = "some-mirror"
233
234 [source.some-mirror]
235 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
236 "#,
237 )).unwrap();
238
239 let config = CargoConfig::try_from_path(&config).unwrap();
240 assert_eq!(
241 config.get_registry_index_url_by_name("some-mirror"),
242 Some("https://artmirror.mycompany/artifactory/cargo-mirror.git"),
243 );
244 }
245
246 #[test]
registry_settings_get_index_url_by_name_from_registry()247 fn registry_settings_get_index_url_by_name_from_registry() {
248 let temp_dir = tempfile::tempdir().unwrap();
249 let config = temp_dir.as_ref().join("config.toml");
250
251 fs::write(&config, textwrap::dedent(
252 r#"
253 [registries]
254 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
255
256 [source.crates-io]
257 replace-with = "art-crates-remote"
258 "#,
259 )).unwrap();
260
261 let config = CargoConfig::try_from_path(&config).unwrap();
262 assert_eq!(
263 config.get_registry_index_url_by_name("art-crates-remote"),
264 Some("https://artprod.mycompany/artifactory/git/cargo-remote.git"),
265 );
266 }
267
268 #[test]
registry_settings_get_source_from_url()269 fn registry_settings_get_source_from_url() {
270 let temp_dir = tempfile::tempdir().unwrap();
271 let config = temp_dir.as_ref().join("config.toml");
272
273 fs::write(
274 &config,
275 textwrap::dedent(
276 r#"
277 [source.some-mirror]
278 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
279 "#,
280 ),
281 )
282 .unwrap();
283
284 let config = CargoConfig::try_from_path(&config).unwrap();
285 assert_eq!(
286 config
287 .get_source_from_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
288 .map(|s| s.registry.as_str()),
289 Some("https://artmirror.mycompany/artifactory/cargo-mirror.git"),
290 );
291 }
292
293 #[test]
resolve_replacement_url_no_replacement()294 fn resolve_replacement_url_no_replacement() {
295 let temp_dir = tempfile::tempdir().unwrap();
296 let config = temp_dir.as_ref().join("config.toml");
297
298 fs::write(&config, "").unwrap();
299
300 let config = CargoConfig::try_from_path(&config).unwrap();
301
302 assert_eq!(
303 config
304 .resolve_replacement_url(utils::CRATES_IO_INDEX_URL)
305 .unwrap(),
306 utils::CRATES_IO_INDEX_URL
307 );
308 assert_eq!(
309 config
310 .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
311 .unwrap(),
312 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
313 );
314 }
315
316 #[test]
resolve_replacement_url_registry()317 fn resolve_replacement_url_registry() {
318 let temp_dir = tempfile::tempdir().unwrap();
319 let config = temp_dir.as_ref().join("config.toml");
320
321 fs::write(&config, textwrap::dedent(
322 r#"
323 [registries]
324 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
325
326 [source.crates-io]
327 replace-with = "some-mirror"
328
329 [source.some-mirror]
330 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
331 "#,
332 )).unwrap();
333
334 let config = CargoConfig::try_from_path(&config).unwrap();
335 assert_eq!(
336 config
337 .resolve_replacement_url(utils::CRATES_IO_INDEX_URL)
338 .unwrap(),
339 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
340 );
341 assert_eq!(
342 config
343 .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
344 .unwrap(),
345 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
346 );
347 assert_eq!(
348 config
349 .resolve_replacement_url(
350 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
351 )
352 .unwrap(),
353 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
354 );
355 }
356
357 #[test]
resolve_replacement_url_source()358 fn resolve_replacement_url_source() {
359 let temp_dir = tempfile::tempdir().unwrap();
360 let config = temp_dir.as_ref().join("config.toml");
361
362 fs::write(&config, textwrap::dedent(
363 r#"
364 [registries]
365 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
366
367 [source.crates-io]
368 replace-with = "art-crates-remote"
369
370 [source.some-mirror]
371 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
372 "#,
373 )).unwrap();
374
375 let config = CargoConfig::try_from_path(&config).unwrap();
376 assert_eq!(
377 config
378 .resolve_replacement_url(utils::CRATES_IO_INDEX_URL)
379 .unwrap(),
380 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
381 );
382 assert_eq!(
383 config
384 .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
385 .unwrap(),
386 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
387 );
388 assert_eq!(
389 config
390 .resolve_replacement_url(
391 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
392 )
393 .unwrap(),
394 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
395 );
396 }
397 }
398