1#[cfg(test)]
4#[path = "auth_tests.rs"]
5mod tests;
6
7use std::env;
8use std::io;
9use std::process::Command;
10
11use crate_seq_ledger::LedgerAuth;
12
13#[derive(Debug, thiserror::Error)]
15pub enum TokenError {
16 #[error("token_env '{var}' is set but empty")]
18 EnvVarEmpty {
19 var: String,
21 },
22 #[error("token_cmd failed: {reason}")]
24 CmdFailed {
25 reason: String,
27 },
28 #[error("failed to read cargo credentials: {0}")]
30 CargoCredentials(#[source] io::Error),
31 #[error("no token found; tried: {tried}")]
33 NotFound {
34 tried: String,
36 },
37}
38
39fn home_dir() -> Option<String> {
41 env::var("HOME")
42 .ok()
43 .or_else(|| env::var("USERPROFILE").ok())
44}
45
46fn try_env(var_name: &str) -> Result<Option<String>, TokenError> {
55 match env::var(var_name) {
56 Ok(val) if val.is_empty() => Err(TokenError::EnvVarEmpty {
57 var: var_name.to_owned(),
58 }),
59 Ok(val) => Ok(Some(val)),
60 Err(env::VarError::NotPresent) => Ok(None),
61 Err(env::VarError::NotUnicode(_)) => Err(TokenError::EnvVarEmpty {
62 var: var_name.to_owned(),
63 }),
64 }
65}
66
67fn try_cmd(cmd: &str) -> Result<Option<String>, TokenError> {
77 let output =
78 Command::new("sh")
79 .args(["-c", cmd])
80 .output()
81 .map_err(|e| TokenError::CmdFailed {
82 reason: e.to_string(),
83 })?;
84
85 let stdout = String::from_utf8_lossy(&output.stdout);
86 let trimmed = stdout.trim();
87
88 if output.status.success() && !trimmed.is_empty() {
89 return Ok(Some(trimmed.to_owned()));
90 }
91
92 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_owned();
93 Err(TokenError::CmdFailed {
94 reason: if stderr.is_empty() {
95 format!("command exited with status {}", output.status)
96 } else {
97 stderr
98 },
99 })
100}
101
102fn try_cargo_credentials_at(
114 path_override: Option<&std::path::Path>,
115) -> Result<Option<String>, TokenError> {
116 let path = if let Some(p) = path_override {
117 p.to_owned()
118 } else {
119 let Some(home) = home_dir() else {
120 return Ok(None);
121 };
122 std::path::PathBuf::from(home)
123 .join(".cargo")
124 .join("credentials.toml")
125 };
126
127 let content = match std::fs::read_to_string(&path) {
128 Ok(s) => s,
129 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
130 Err(e) => return Err(TokenError::CargoCredentials(e)),
131 };
132
133 let doc: toml_edit::DocumentMut = content.parse().map_err(|_| {
134 TokenError::CargoCredentials(io::Error::new(
135 io::ErrorKind::InvalidData,
136 "credentials.toml is not valid TOML",
137 ))
138 })?;
139
140 let token = doc
141 .get("registry")
142 .and_then(|r| r.get("token"))
143 .and_then(|t| t.as_str())
144 .map(ToOwned::to_owned);
145
146 Ok(token)
147}
148
149pub fn resolve_token(
166 cli_token: Option<&str>,
167 ledger_auth: &LedgerAuth,
168) -> Result<Option<String>, TokenError> {
169 resolve_token_inner(cli_token, ledger_auth, None)
170}
171
172pub(crate) fn resolve_token_inner(
174 cli_token: Option<&str>,
175 ledger_auth: &LedgerAuth,
176 creds_path_override: Option<&std::path::Path>,
177) -> Result<Option<String>, TokenError> {
178 if let Some(t) = cli_token {
179 return Ok(Some(t.to_owned()));
180 }
181
182 if let Some(ref var) = ledger_auth.token_env {
183 if let Some(val) = try_env(var)? {
184 return Ok(Some(val));
185 }
186 } else if let Some(ref cmd) = ledger_auth.token_cmd {
188 return try_cmd(cmd);
189 }
190
191 try_cargo_credentials_at(creds_path_override)
192}
193
194pub fn require_token(
205 cli_token: Option<&str>,
206 ledger_auth: &LedgerAuth,
207) -> Result<Option<String>, crate::Error> {
208 match resolve_token(cli_token, ledger_auth) {
209 Ok(token) => Ok(token),
210 Err(TokenError::EnvVarEmpty { var }) => Err(crate::Error::TokenResolution(format!(
211 "env var '{var}' is set but empty; set it to your crates.io token"
212 ))),
213 Err(TokenError::CmdFailed { reason }) => Err(crate::Error::TokenResolution(format!(
214 "token_cmd failed: {reason}\nCheck the command in your .crate-seq.toml [auth] section"
215 ))),
216 Err(TokenError::CargoCredentials(e)) => Err(crate::Error::TokenResolution(format!(
217 "could not read ~/.cargo/credentials.toml: {e}\nRun `cargo login` or set [auth] in .crate-seq.toml"
218 ))),
219 Err(TokenError::NotFound { tried }) => Err(crate::Error::TokenResolution(format!(
220 "no token found (tried: {tried})"
221 ))),
222 }
223}