use std::{fs::File, io, path::Path};
use binstalk_types::SecretString;
use compact_str::CompactString;
use fs_lock::FileLock;
use miette::Diagnostic;
use serde::{Deserialize, Deserializer};
use thiserror::Error;
#[derive(Clone, Debug, Default, Deserialize)]
pub struct RegistryCredential {
#[serde(default, deserialize_with = "deserialize_secret_string")]
pub token: Option<SecretString>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Credentials {
pub registries: Option<std::collections::BTreeMap<CompactString, RegistryCredential>>,
pub registry: Option<RegistryCredential>,
}
impl Credentials {
pub fn load_from_reader<R: io::Read>(reader: R) -> Result<Self, CredentialsLoadError> {
let mut reader = reader;
let mut contents = Vec::new();
reader.read_to_end(&mut contents)?;
let credentials: Credentials = toml_edit::de::from_slice(&contents)?;
Ok(credentials)
}
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, CredentialsLoadError> {
match File::open(path.as_ref()) {
Ok(file) => {
let file = FileLock::new_shared(file)?.set_file_path(path.as_ref());
Self::load_from_reader(file)
}
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(Default::default()),
Err(err) => Err(err.into()),
}
}
pub fn load_from_home(cargo_home: impl AsRef<Path>) -> Result<Self, CredentialsLoadError> {
let cargo_home = cargo_home.as_ref();
let toml_path = cargo_home.join("credentials.toml");
match File::open(&toml_path) {
Ok(file) => {
let file = FileLock::new_shared(file)?.set_file_path(toml_path.as_path());
Self::load_from_reader(file)
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
Self::load_from_path(cargo_home.join("credentials"))
}
Err(err) => Err(err.into()),
}
}
pub fn get_registry_token(&self, name: &str) -> Option<&SecretString> {
self.registries.as_ref()?.get(name)?.token.as_ref()
}
}
fn deserialize_secret_string<'de, D>(deserializer: D) -> Result<Option<SecretString>, D::Error>
where
D: Deserializer<'de>,
{
Option::<Box<str>>::deserialize(deserializer)
.map(|value| value.map(SecretString::from_boxed_str))
}
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
pub enum CredentialsLoadError {
#[error("I/O Error: {0}")]
Io(#[from] io::Error),
#[error("Failed to deserialize toml: {0}")]
TomlParse(Box<toml_edit::de::Error>),
}
impl From<toml_edit::de::Error> for CredentialsLoadError {
fn from(e: toml_edit::de::Error) -> Self {
CredentialsLoadError::TomlParse(Box::new(e))
}
}
impl From<toml_edit::TomlError> for CredentialsLoadError {
fn from(e: toml_edit::TomlError) -> Self {
CredentialsLoadError::TomlParse(Box::new(e.into()))
}
}
#[cfg(test)]
mod tests {
use std::{fs, io::Cursor};
use tempfile::tempdir;
use super::*;
#[test]
fn test_loading() {
const CREDENTIALS: &str = r#"
[registry]
token = "crates-io-token"
[registries.private-registry]
token = "private-token"
"#;
let credentials = Credentials::load_from_reader(Cursor::new(CREDENTIALS)).unwrap();
assert_eq!(
credentials
.get_registry_token("private-registry")
.map(|token| &token[..]),
Some("private-token")
);
}
#[test]
fn test_load_from_home_prefers_toml_path() {
let tempdir = tempdir().unwrap();
let home = tempdir.path();
fs::write(
home.join("credentials"),
"[registries.example]\ntoken = \"legacy\"\n",
)
.unwrap();
fs::write(
home.join("credentials.toml"),
"[registries.example]\ntoken = \"toml\"\n",
)
.unwrap();
let credentials = Credentials::load_from_home(home).unwrap();
assert_eq!(
credentials
.get_registry_token("example")
.map(|token| &token[..]),
Some("toml")
);
}
#[test]
fn test_registry_credential_debug_redacts_token() {
let credential = RegistryCredential {
token: Some(SecretString::from_boxed_str("secret-token".into())),
};
let debug = format!("{credential:?}");
assert!(!debug.contains("secret-token"));
assert!(debug.contains("<redacted>"));
}
}