cargo_utils/
token.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use anyhow::Context as _;
4use secrecy::SecretString;
5use serde::Deserialize;
6
7pub fn registry_token(registry: Option<&str>) -> anyhow::Result<Option<SecretString>> {
8    let mut token = registry_token_from_env(registry);
9    if token.is_none() {
10        token = registry_token_from_credential_file(registry).with_context(|| {
11            format!(
12                "can't retreive token from credential file for registry `{}`",
13                registry.unwrap_or("crates.io"),
14            )
15        })?;
16    }
17    Ok(token)
18}
19
20/// Read credentials for a specific registry using environment variables.
21/// <https://doc.rust-lang.org/cargo/reference/registry-authentication.html#cargotoken>
22pub fn registry_token_from_env(registry: Option<&str>) -> Option<SecretString> {
23    let token = if let Some(r) = registry {
24        let env_var = format!("CARGO_REGISTRIES_{}_TOKEN", r.to_uppercase());
25        std::env::var(env_var)
26    } else {
27        std::env::var("CARGO_REGISTRY_TOKEN")
28    };
29    token.ok().map(|t| t.into())
30}
31
32/// Read credentials for a specific registry using file cargo/credentials.toml.
33/// <https://doc.rust-lang.org/cargo/reference/config.html#credentials>
34pub fn registry_token_from_credential_file(
35    registry: Option<&str>,
36) -> anyhow::Result<Option<SecretString>> {
37    let credentials = read_cargo_credentials()?;
38    let token = credentials
39        .and_then(|c| {
40            let token: Option<RegistryToken> = if let Some(r) = registry {
41                c.registries.get(r).cloned()
42            } else {
43                c.registry.as_ref().cloned()
44            };
45            token
46        })
47        .and_then(|r| r.token.clone())
48        .map(|t| t.into());
49    Ok(token)
50}
51
52fn read_cargo_credentials() -> anyhow::Result<Option<CargoCredentials>> {
53    let credentials_path = credentials_path()?;
54    let credentials = if let Some(credentials_path) = credentials_path {
55        let content = fs_err::read_to_string(&credentials_path)
56            .context("failed to read cargo credentials file")?;
57        let credentials = toml::from_str::<CargoCredentials>(&content)
58            .context("Invalid cargo credentials file")?;
59        Some(credentials)
60    } else {
61        None
62    };
63    Ok(credentials)
64}
65
66fn credentials_path() -> anyhow::Result<Option<PathBuf>> {
67    let cargo_home = crate::cargo_home()?;
68    let mut path = cargo_home.join("credentials.toml");
69    if !path.exists() {
70        path = cargo_home.join("credentials");
71    }
72    if !path.exists() {
73        return Ok(None);
74    }
75    Ok(Some(path))
76}
77
78#[derive(Debug, Deserialize, Default, PartialEq)]
79struct CargoCredentials {
80    #[serde(default)]
81    registry: Option<RegistryToken>,
82    #[serde(default)]
83    registries: HashMap<String, RegistryToken>,
84}
85
86#[derive(Debug, Deserialize, Default, PartialEq, Clone)]
87struct RegistryToken {
88    token: Option<String>,
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_parse_cargo_credentials_both() {
97        let sample = r#"
98            [registry]
99            token = "aaaa"   # Access token for crates.io
100
101            [registries.my]
102            token = "bbb"   # Access token for the named registry
103        "#;
104        let creds = toml::from_str::<CargoCredentials>(sample).unwrap();
105        assert_eq!(
106            creds.registry.and_then(|r| r.token),
107            Some("aaaa".to_string())
108        );
109        assert_eq!(
110            creds.registries.get("my").and_then(|r| r.token.clone()),
111            Some("bbb".to_string())
112        );
113        assert_eq!(
114            creds.registries.get("foo").and_then(|r| r.token.clone()),
115            None
116        );
117    }
118
119    #[test]
120    fn test_parse_cargo_credentials_cratesio_only() {
121        let sample = r#"
122            [registry]
123            token = "aaaa"   # Access token for crates.io
124        "#;
125        let creds = toml::from_str::<CargoCredentials>(sample).unwrap();
126        assert_eq!(
127            creds.registry.and_then(|r| r.token),
128            Some("aaaa".to_string())
129        );
130        assert_eq!(
131            creds.registries.get("my").and_then(|r| r.token.clone()),
132            None
133        );
134        assert_eq!(
135            creds.registries.get("foo").and_then(|r| r.token.clone()),
136            None
137        );
138    }
139
140    #[test]
141    fn test_parse_cargo_credentials_empty() {
142        let sample = "";
143        let creds = toml::from_str::<CargoCredentials>(sample).unwrap();
144        assert_eq!(creds.registry.and_then(|r| r.token), None);
145        assert_eq!(
146            creds.registries.get("my").and_then(|r| r.token.clone()),
147            None
148        );
149        assert_eq!(
150            creds.registries.get("foo").and_then(|r| r.token.clone()),
151            None
152        );
153    }
154}