binstalk_manifests/
cargo_credentials.rs1use std::{fs::File, io, path::Path};
7
8use binstalk_types::SecretString;
9use compact_str::CompactString;
10use fs_lock::FileLock;
11use miette::Diagnostic;
12use serde::{Deserialize, Deserializer};
13use thiserror::Error;
14
15#[derive(Clone, Debug, Default, Deserialize)]
16pub struct RegistryCredential {
17 #[serde(default, deserialize_with = "deserialize_secret_string")]
18 pub token: Option<SecretString>,
19}
20
21#[derive(Clone, Debug, Default, Deserialize)]
22pub struct Credentials {
23 pub registries: Option<std::collections::BTreeMap<CompactString, RegistryCredential>>,
24 pub registry: Option<RegistryCredential>,
25}
26
27impl Credentials {
28 pub fn load_from_reader<R: io::Read>(reader: R) -> Result<Self, CredentialsLoadError> {
29 let mut reader = reader;
30 let mut contents = Vec::new();
31 reader.read_to_end(&mut contents)?;
32 let credentials: Credentials = toml_edit::de::from_slice(&contents)?;
33 Ok(credentials)
34 }
35
36 pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, CredentialsLoadError> {
37 match File::open(path.as_ref()) {
38 Ok(file) => {
39 let file = FileLock::new_shared(file)?.set_file_path(path.as_ref());
40 Self::load_from_reader(file)
41 }
42 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Default::default()),
43 Err(err) => Err(err.into()),
44 }
45 }
46
47 pub fn load_from_home(cargo_home: impl AsRef<Path>) -> Result<Self, CredentialsLoadError> {
48 let cargo_home = cargo_home.as_ref();
49 let toml_path = cargo_home.join("credentials.toml");
50
51 match File::open(&toml_path) {
52 Ok(file) => {
53 let file = FileLock::new_shared(file)?.set_file_path(toml_path.as_path());
54 Self::load_from_reader(file)
55 }
56 Err(err) if err.kind() == io::ErrorKind::NotFound => {
57 Self::load_from_path(cargo_home.join("credentials"))
58 }
59 Err(err) => Err(err.into()),
60 }
61 }
62
63 pub fn get_registry_token(&self, name: &str) -> Option<&SecretString> {
64 self.registries.as_ref()?.get(name)?.token.as_ref()
65 }
66}
67
68fn deserialize_secret_string<'de, D>(deserializer: D) -> Result<Option<SecretString>, D::Error>
69where
70 D: Deserializer<'de>,
71{
72 Option::<Box<str>>::deserialize(deserializer)
73 .map(|value| value.map(SecretString::from_boxed_str))
74}
75
76#[derive(Debug, Diagnostic, Error)]
77#[non_exhaustive]
78pub enum CredentialsLoadError {
79 #[error("I/O Error: {0}")]
80 Io(#[from] io::Error),
81
82 #[error("Failed to deserialize toml: {0}")]
83 TomlParse(Box<toml_edit::de::Error>),
84}
85
86impl From<toml_edit::de::Error> for CredentialsLoadError {
87 fn from(e: toml_edit::de::Error) -> Self {
88 CredentialsLoadError::TomlParse(Box::new(e))
89 }
90}
91
92impl From<toml_edit::TomlError> for CredentialsLoadError {
93 fn from(e: toml_edit::TomlError) -> Self {
94 CredentialsLoadError::TomlParse(Box::new(e.into()))
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use std::{fs, io::Cursor};
101
102 use tempfile::tempdir;
103
104 use super::*;
105
106 #[test]
107 fn test_loading() {
108 const CREDENTIALS: &str = r#"
109[registry]
110token = "crates-io-token"
111
112[registries.private-registry]
113token = "private-token"
114 "#;
115
116 let credentials = Credentials::load_from_reader(Cursor::new(CREDENTIALS)).unwrap();
117
118 assert_eq!(
119 credentials
120 .get_registry_token("private-registry")
121 .map(|token| &token[..]),
122 Some("private-token")
123 );
124 }
125
126 #[test]
127 fn test_load_from_home_prefers_toml_path() {
128 let tempdir = tempdir().unwrap();
129 let home = tempdir.path();
130
131 fs::write(
132 home.join("credentials"),
133 "[registries.example]\ntoken = \"legacy\"\n",
134 )
135 .unwrap();
136 fs::write(
137 home.join("credentials.toml"),
138 "[registries.example]\ntoken = \"toml\"\n",
139 )
140 .unwrap();
141
142 let credentials = Credentials::load_from_home(home).unwrap();
143
144 assert_eq!(
145 credentials
146 .get_registry_token("example")
147 .map(|token| &token[..]),
148 Some("toml")
149 );
150 }
151
152 #[test]
153 fn test_registry_credential_debug_redacts_token() {
154 let credential = RegistryCredential {
155 token: Some(SecretString::from_boxed_str("secret-token".into())),
156 };
157
158 let debug = format!("{credential:?}");
159
160 assert!(!debug.contains("secret-token"));
161 assert!(debug.contains("<redacted>"));
162 }
163}