1use std::env;
4use std::io;
5use std::process::Command;
6
7use crate_seq_ledger::LedgerAuth;
8
9#[derive(Debug, thiserror::Error)]
11pub enum TokenError {
12 #[error("token_env '{var}' is set but empty")]
14 EnvVarEmpty {
15 var: String,
17 },
18 #[error("token_cmd failed: {reason}")]
20 CmdFailed {
21 reason: String,
23 },
24 #[error("failed to read cargo credentials: {0}")]
26 CargoCredentials(#[source] io::Error),
27 #[error("no token found; tried: {tried}")]
29 NotFound {
30 tried: String,
32 },
33}
34
35fn home_dir() -> Option<String> {
37 env::var("HOME").ok().or_else(|| env::var("USERPROFILE").ok())
38}
39
40fn try_env(var_name: &str) -> Result<Option<String>, TokenError> {
49 match env::var(var_name) {
50 Ok(val) if val.is_empty() => Err(TokenError::EnvVarEmpty {
51 var: var_name.to_owned(),
52 }),
53 Ok(val) => Ok(Some(val)),
54 Err(env::VarError::NotPresent) => Ok(None),
55 Err(env::VarError::NotUnicode(_)) => Err(TokenError::EnvVarEmpty {
56 var: var_name.to_owned(),
57 }),
58 }
59}
60
61fn try_cmd(cmd: &str) -> Result<Option<String>, TokenError> {
71 let output = Command::new("sh")
72 .args(["-c", cmd])
73 .output()
74 .map_err(|e| TokenError::CmdFailed {
75 reason: e.to_string(),
76 })?;
77
78 let stdout = String::from_utf8_lossy(&output.stdout);
79 let trimmed = stdout.trim();
80
81 if output.status.success() && !trimmed.is_empty() {
82 return Ok(Some(trimmed.to_owned()));
83 }
84
85 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
86 Err(TokenError::CmdFailed {
87 reason: if stderr.is_empty() {
88 format!("command exited with status {}", output.status)
89 } else {
90 stderr
91 },
92 })
93}
94
95fn try_cargo_credentials() -> Result<Option<String>, TokenError> {
104 let Some(home) = home_dir() else {
105 return Ok(None);
106 };
107
108 let path = std::path::PathBuf::from(home)
109 .join(".cargo")
110 .join("credentials.toml");
111
112 let content = match std::fs::read_to_string(&path) {
113 Ok(s) => s,
114 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
115 Err(e) => return Err(TokenError::CargoCredentials(e)),
116 };
117
118 let doc: toml_edit::DocumentMut = content.parse().map_err(|_| {
119 TokenError::CargoCredentials(io::Error::new(
120 io::ErrorKind::InvalidData,
121 "credentials.toml is not valid TOML",
122 ))
123 })?;
124
125 let token = doc
126 .get("registry")
127 .and_then(|r| r.get("token"))
128 .and_then(|t| t.as_str())
129 .map(ToOwned::to_owned);
130
131 Ok(token)
132}
133
134pub fn resolve_token(
151 cli_token: Option<&str>,
152 ledger_auth: &LedgerAuth,
153) -> Result<Option<String>, TokenError> {
154 if let Some(t) = cli_token {
155 return Ok(Some(t.to_owned()));
156 }
157
158 if let Some(ref var) = ledger_auth.token_env {
159 if let Some(val) = try_env(var)? {
160 return Ok(Some(val));
161 }
162 } else if let Some(ref cmd) = ledger_auth.token_cmd {
164 return try_cmd(cmd);
165 }
166
167 try_cargo_credentials()
168}
169
170pub fn require_token(
181 cli_token: Option<&str>,
182 ledger_auth: &LedgerAuth,
183) -> Result<Option<String>, crate::Error> {
184 match resolve_token(cli_token, ledger_auth) {
185 Ok(token) => Ok(token),
186 Err(TokenError::EnvVarEmpty { var }) => Err(crate::Error::TokenResolution(format!(
187 "env var '{var}' is set but empty; set it to your crates.io token"
188 ))),
189 Err(TokenError::CmdFailed { reason }) => Err(crate::Error::TokenResolution(format!(
190 "token_cmd failed: {reason}\nCheck the command in your .crate-seq.toml [auth] section"
191 ))),
192 Err(TokenError::CargoCredentials(e)) => Err(crate::Error::TokenResolution(format!(
193 "could not read ~/.cargo/credentials.toml: {e}\nRun `cargo login` or set [auth] in .crate-seq.toml"
194 ))),
195 Err(TokenError::NotFound { tried }) => Err(crate::Error::TokenResolution(format!(
196 "no token found (tried: {tried})"
197 ))),
198 }
199}