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 #[test]
registry_settings()167 fn registry_settings() {
168 let temp_dir = tempfile::tempdir().unwrap();
169 let config = temp_dir.as_ref().join("config.toml");
170
171 fs::write(&config, textwrap::dedent(
172 r#"
173 # Makes artifactory the default registry and saves passing --registry parameter
174 [registry]
175 default = "art-crates-remote"
176
177 [registries]
178 # Remote repository proxy in Artifactory (read-only)
179 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
180
181 # Optional, use with --registry to publish to crates.io
182 crates-io = { index = "https://github.com/rust-lang/crates.io-index" }
183
184 [net]
185 git-fetch-with-cli = true
186 "#,
187 )).unwrap();
188
189 let config = CargoConfig::try_from_path(&config).unwrap();
190 assert_eq!(
191 config,
192 CargoConfig {
193 registries: BTreeMap::from([
194 (
195 "art-crates-remote".to_owned(),
196 AdditionalRegistry {
197 index: "https://artprod.mycompany/artifactory/git/cargo-remote.git"
198 .to_owned(),
199 token: None,
200 },
201 ),
202 (
203 "crates-io".to_owned(),
204 AdditionalRegistry {
205 index: "https://github.com/rust-lang/crates.io-index".to_owned(),
206 token: None,
207 },
208 ),
209 ]),
210 registry: Registry {
211 default: "art-crates-remote".to_owned(),
212 token: None,
213 },
214 source: BTreeMap::new(),
215 },
216 )
217 }
218
219 #[test]
registry_settings_get_index_url_by_name_from_source()220 fn registry_settings_get_index_url_by_name_from_source() {
221 let temp_dir = tempfile::tempdir().unwrap();
222 let config = temp_dir.as_ref().join("config.toml");
223
224 fs::write(&config, textwrap::dedent(
225 r#"
226 [registries]
227 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
228
229 [source.crates-io]
230 replace-with = "some-mirror"
231
232 [source.some-mirror]
233 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
234 "#,
235 )).unwrap();
236
237 let config = CargoConfig::try_from_path(&config).unwrap();
238 assert_eq!(
239 config.get_registry_index_url_by_name("some-mirror"),
240 Some("https://artmirror.mycompany/artifactory/cargo-mirror.git"),
241 );
242 }
243
244 #[test]
registry_settings_get_index_url_by_name_from_registry()245 fn registry_settings_get_index_url_by_name_from_registry() {
246 let temp_dir = tempfile::tempdir().unwrap();
247 let config = temp_dir.as_ref().join("config.toml");
248
249 fs::write(&config, textwrap::dedent(
250 r#"
251 [registries]
252 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
253
254 [source.crates-io]
255 replace-with = "art-crates-remote"
256 "#,
257 )).unwrap();
258
259 let config = CargoConfig::try_from_path(&config).unwrap();
260 assert_eq!(
261 config.get_registry_index_url_by_name("art-crates-remote"),
262 Some("https://artprod.mycompany/artifactory/git/cargo-remote.git"),
263 );
264 }
265
266 #[test]
registry_settings_get_source_from_url()267 fn registry_settings_get_source_from_url() {
268 let temp_dir = tempfile::tempdir().unwrap();
269 let config = temp_dir.as_ref().join("config.toml");
270
271 fs::write(
272 &config,
273 textwrap::dedent(
274 r#"
275 [source.some-mirror]
276 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
277 "#,
278 ),
279 )
280 .unwrap();
281
282 let config = CargoConfig::try_from_path(&config).unwrap();
283 assert_eq!(
284 config
285 .get_source_from_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
286 .map(|s| s.registry.as_str()),
287 Some("https://artmirror.mycompany/artifactory/cargo-mirror.git"),
288 );
289 }
290
291 #[test]
resolve_replacement_url_no_replacement()292 fn resolve_replacement_url_no_replacement() {
293 let temp_dir = tempfile::tempdir().unwrap();
294 let config = temp_dir.as_ref().join("config.toml");
295
296 fs::write(&config, "").unwrap();
297
298 let config = CargoConfig::try_from_path(&config).unwrap();
299
300 assert_eq!(
301 config
302 .resolve_replacement_url(utils::CRATES_IO_INDEX_URL)
303 .unwrap(),
304 utils::CRATES_IO_INDEX_URL
305 );
306 assert_eq!(
307 config
308 .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
309 .unwrap(),
310 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
311 );
312 }
313
314 #[test]
resolve_replacement_url_registry()315 fn resolve_replacement_url_registry() {
316 let temp_dir = tempfile::tempdir().unwrap();
317 let config = temp_dir.as_ref().join("config.toml");
318
319 fs::write(&config, textwrap::dedent(
320 r#"
321 [registries]
322 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
323
324 [source.crates-io]
325 replace-with = "some-mirror"
326
327 [source.some-mirror]
328 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
329 "#,
330 )).unwrap();
331
332 let config = CargoConfig::try_from_path(&config).unwrap();
333 assert_eq!(
334 config
335 .resolve_replacement_url(utils::CRATES_IO_INDEX_URL)
336 .unwrap(),
337 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
338 );
339 assert_eq!(
340 config
341 .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
342 .unwrap(),
343 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
344 );
345 assert_eq!(
346 config
347 .resolve_replacement_url(
348 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
349 )
350 .unwrap(),
351 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
352 );
353 }
354
355 #[test]
resolve_replacement_url_source()356 fn resolve_replacement_url_source() {
357 let temp_dir = tempfile::tempdir().unwrap();
358 let config = temp_dir.as_ref().join("config.toml");
359
360 fs::write(&config, textwrap::dedent(
361 r#"
362 [registries]
363 art-crates-remote = { index = "https://artprod.mycompany/artifactory/git/cargo-remote.git" }
364
365 [source.crates-io]
366 replace-with = "art-crates-remote"
367
368 [source.some-mirror]
369 registry = "https://artmirror.mycompany/artifactory/cargo-mirror.git"
370 "#,
371 )).unwrap();
372
373 let config = CargoConfig::try_from_path(&config).unwrap();
374 assert_eq!(
375 config
376 .resolve_replacement_url(utils::CRATES_IO_INDEX_URL)
377 .unwrap(),
378 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
379 );
380 assert_eq!(
381 config
382 .resolve_replacement_url("https://artmirror.mycompany/artifactory/cargo-mirror.git")
383 .unwrap(),
384 "https://artmirror.mycompany/artifactory/cargo-mirror.git"
385 );
386 assert_eq!(
387 config
388 .resolve_replacement_url(
389 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
390 )
391 .unwrap(),
392 "https://artprod.mycompany/artifactory/git/cargo-remote.git"
393 );
394 }
395 }
396