Skip to content

Commit a81d558

Browse files
committed
Use thiserror for credential provider errors
1 parent 5321146 commit a81d558

File tree

13 files changed

+251
-144
lines changed

13 files changed

+251
-144
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

credential/cargo-credential-1password/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ impl OnePasswordKeychain {
243243
Some(password) => password
244244
.value
245245
.map(Secret::from)
246-
.ok_or_else(|| format!("missing password value for entry").into()),
246+
.ok_or("missing password value for entry".into()),
247247
None => Err("could not find password field".into()),
248248
}
249249
}

credential/cargo-credential-macos-keychain/src/lib.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ mod macos {
1717
format!("cargo-registry:{}", index_url)
1818
}
1919

20-
fn to_credential_error(e: security_framework::base::Error) -> Error {
21-
Error::Other(format!("security framework ({}): {e}", e.code()))
22-
}
23-
2420
impl Credential for MacKeychain {
2521
fn perform(
2622
&self,
@@ -34,11 +30,9 @@ mod macos {
3430
match action {
3531
Action::Get(_) => match keychain.find_generic_password(&service_name, ACCOUNT) {
3632
Err(e) if e.code() == not_found => Err(Error::NotFound),
37-
Err(e) => Err(to_credential_error(e)),
33+
Err(e) => Err(Box::new(e).into()),
3834
Ok((pass, _)) => {
39-
let token = String::from_utf8(pass.as_ref().to_vec()).map_err(|_| {
40-
Error::Other("failed to convert token to UTF8".to_string())
41-
})?;
35+
let token = String::from_utf8(pass.as_ref().to_vec()).map_err(Box::new)?;
4236
Ok(CredentialResponse::Get {
4337
token: token.into(),
4438
cache: CacheControl::Session,
@@ -57,19 +51,19 @@ mod macos {
5751
ACCOUNT,
5852
token.expose().as_bytes(),
5953
)
60-
.map_err(to_credential_error)?;
54+
.map_err(Box::new)?;
6155
}
6256
}
6357
Ok((_, mut item)) => {
6458
item.set_password(token.expose().as_bytes())
65-
.map_err(to_credential_error)?;
59+
.map_err(Box::new)?;
6660
}
6761
}
6862
Ok(CredentialResponse::Login)
6963
}
7064
Action::Logout => match keychain.find_generic_password(&service_name, ACCOUNT) {
7165
Err(e) if e.code() == not_found => Err(Error::NotFound),
72-
Err(e) => Err(to_credential_error(e)),
66+
Err(e) => Err(Box::new(e).into()),
7367
Ok((_, item)) => {
7468
item.delete();
7569
Ok(CredentialResponse::Logout)

credential/cargo-credential-wincred/src/lib.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,13 @@ mod win {
6565
(*p_credential).CredentialBlobSize as usize,
6666
)
6767
};
68-
let result = match String::from_utf8(bytes.to_vec()) {
69-
Err(_) => Err("failed to convert token to UTF8".into()),
70-
Ok(token) => Ok(CredentialResponse::Get {
71-
token: token.into(),
72-
cache: CacheControl::Session,
73-
operation_independent: true,
74-
}),
75-
};
76-
let _ = unsafe { CredFree(p_credential as *mut _) };
77-
result
68+
let token = String::from_utf8(bytes.to_vec()).map_err(Box::new);
69+
unsafe { CredFree(p_credential as *mut _) };
70+
Ok(CredentialResponse::Get {
71+
token: token?.into(),
72+
cache: CacheControl::Session,
73+
operation_independent: true,
74+
})
7875
}
7976
Action::Login(options) => {
8077
let token = read_token(options, registry)?.expose();

credential/cargo-credential/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ repository = "https://github.com/rust-lang/cargo"
77
description = "A library to assist writing Cargo credential helpers."
88

99
[dependencies]
10+
anyhow.workspace = true
1011
serde = { workspace = true, features = ["derive"] }
1112
serde_json.workspace = true
13+
thiserror.workspace = true
1214
time.workspace = true
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
use serde::{Deserialize, Serialize};
2+
use std::error::Error as StdError;
3+
use thiserror::Error as ThisError;
4+
5+
/// Credential provider error type.
6+
///
7+
/// `UrlNotSupported` and `NotFound` errors both cause Cargo
8+
/// to attempt another provider, if one is available. The other
9+
/// variants are fatal.
10+
///
11+
/// Note: Do not add a tuple variant, as it cannot be serialized.
12+
#[derive(Serialize, Deserialize, ThisError, Debug)]
13+
#[serde(rename_all = "kebab-case", tag = "kind")]
14+
#[non_exhaustive]
15+
pub enum Error {
16+
#[error("registry not supported")]
17+
UrlNotSupported,
18+
#[error("credential not found")]
19+
NotFound,
20+
#[error("requested operation not supported")]
21+
OperationNotSupported,
22+
#[error("protocol version {version} not supported")]
23+
ProtocolNotSupported { version: u32 },
24+
#[error(transparent)]
25+
#[serde(with = "error_serialize")]
26+
Other(Box<dyn StdError + Sync + Send>),
27+
}
28+
29+
impl From<std::io::Error> for Error {
30+
fn from(err: std::io::Error) -> Self {
31+
Box::new(err).into()
32+
}
33+
}
34+
35+
impl From<serde_json::Error> for Error {
36+
fn from(err: serde_json::Error) -> Self {
37+
Box::new(err).into()
38+
}
39+
}
40+
41+
impl From<String> for Error {
42+
fn from(err: String) -> Self {
43+
Box::new(StringTypedError {
44+
message: err.to_string(),
45+
source: None,
46+
})
47+
.into()
48+
}
49+
}
50+
51+
impl From<&str> for Error {
52+
fn from(err: &str) -> Self {
53+
err.to_string().into()
54+
}
55+
}
56+
57+
impl From<anyhow::Error> for Error {
58+
fn from(value: anyhow::Error) -> Self {
59+
let mut prev = None;
60+
for e in value.chain().rev() {
61+
prev = Some(Box::new(StringTypedError {
62+
message: e.to_string(),
63+
source: prev,
64+
}));
65+
}
66+
Error::Other(prev.unwrap())
67+
}
68+
}
69+
70+
impl<T: StdError + Send + Sync + 'static> From<Box<T>> for Error {
71+
fn from(value: Box<T>) -> Self {
72+
Error::Other(value)
73+
}
74+
}
75+
76+
/// String-based error type with an optional source
77+
#[derive(Debug)]
78+
struct StringTypedError {
79+
message: String,
80+
source: Option<Box<StringTypedError>>,
81+
}
82+
83+
impl StdError for StringTypedError {
84+
fn source(&self) -> Option<&(dyn StdError + 'static)> {
85+
self.source.as_ref().map(|err| err as &dyn StdError)
86+
}
87+
}
88+
89+
impl std::fmt::Display for StringTypedError {
90+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91+
self.message.fmt(f)
92+
}
93+
}
94+
95+
/// Serializer / deserializer for any boxed error.
96+
/// The string representation of the error, and its `source` chain can roundtrip across
97+
/// the serialization. The actual types are lost (downcast will not work).
98+
mod error_serialize {
99+
use std::error::Error as StdError;
100+
use std::ops::Deref;
101+
102+
use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serializer};
103+
104+
use crate::error::StringTypedError;
105+
106+
pub fn serialize<S>(
107+
e: &Box<dyn StdError + Send + Sync>,
108+
serializer: S,
109+
) -> Result<S::Ok, S::Error>
110+
where
111+
S: Serializer,
112+
{
113+
let mut state = serializer.serialize_struct("StringTypedError", 2)?;
114+
state.serialize_field("message", &format!("{}", e))?;
115+
116+
// Serialize the source error chain recursively
117+
let mut current_source: &dyn StdError = e.deref();
118+
let mut sources = Vec::new();
119+
while let Some(err) = current_source.source() {
120+
sources.push(err.to_string());
121+
current_source = err;
122+
}
123+
state.serialize_field("caused-by", &sources)?;
124+
state.end()
125+
}
126+
127+
pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<dyn StdError + Sync + Send>, D::Error>
128+
where
129+
D: Deserializer<'de>,
130+
{
131+
#[derive(Deserialize)]
132+
#[serde(rename_all = "kebab-case")]
133+
struct ErrorData {
134+
message: String,
135+
caused_by: Option<Vec<String>>,
136+
}
137+
let data = ErrorData::deserialize(deserializer)?;
138+
let mut prev = None;
139+
if let Some(source) = data.caused_by {
140+
for e in source.into_iter().rev() {
141+
prev = Some(Box::new(StringTypedError {
142+
message: e,
143+
source: prev,
144+
}));
145+
}
146+
}
147+
let e = Box::new(StringTypedError {
148+
message: data.message,
149+
source: prev,
150+
});
151+
Ok(e)
152+
}
153+
}
154+
155+
#[cfg(test)]
156+
mod tests {
157+
use super::Error;
158+
159+
#[test]
160+
pub fn roundtrip() {
161+
// Construct an error with context
162+
let e = anyhow::anyhow!("E1").context("E2").context("E3");
163+
// Convert to a string with contexts.
164+
let s1 = format!("{:?}", e);
165+
// Convert the error into an `Error`
166+
let e: Error = e.into();
167+
// Convert that error into JSON
168+
let json = serde_json::to_string_pretty(&e).unwrap();
169+
// Convert that error back to anyhow
170+
let e: anyhow::Error = e.into();
171+
let s2 = format!("{:?}", e);
172+
assert_eq!(s1, s2);
173+
174+
// Convert the error back from JSON
175+
let e: Error = serde_json::from_str(&json).unwrap();
176+
// Convert to back to anyhow
177+
let e: anyhow::Error = e.into();
178+
let s3 = format!("{:?}", e);
179+
assert_eq!(s2, s3);
180+
181+
assert_eq!(
182+
r#"{
183+
"kind": "other",
184+
"message": "E3",
185+
"caused-by": [
186+
"E2",
187+
"E1"
188+
]
189+
}"#,
190+
json
191+
);
192+
}
193+
}

credential/cargo-credential/src/lib.rs

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use std::{
1717
};
1818
use time::OffsetDateTime;
1919

20+
mod error;
2021
mod secret;
22+
pub use error::Error;
2123
pub use secret::Secret;
2224

2325
/// Message sent by the credential helper on startup
@@ -163,70 +165,6 @@ pub enum CacheControl {
163165
/// this version will prevent new credential providers
164166
/// from working with older versions of Cargo.
165167
pub const PROTOCOL_VERSION_1: u32 = 1;
166-
167-
#[derive(Serialize, Deserialize, Clone, Debug)]
168-
#[serde(rename_all = "kebab-case", tag = "kind", content = "detail")]
169-
#[non_exhaustive]
170-
pub enum Error {
171-
UrlNotSupported,
172-
ProtocolNotSupported(u32),
173-
Subprocess(String),
174-
Io(String),
175-
Serde(String),
176-
Other(String),
177-
OperationNotSupported,
178-
NotFound,
179-
}
180-
181-
impl From<serde_json::Error> for Error {
182-
fn from(err: serde_json::Error) -> Self {
183-
Error::Serde(err.to_string())
184-
}
185-
}
186-
187-
impl From<std::io::Error> for Error {
188-
fn from(err: std::io::Error) -> Self {
189-
Error::Io(err.to_string())
190-
}
191-
}
192-
193-
impl From<String> for Error {
194-
fn from(err: String) -> Self {
195-
Error::Other(err)
196-
}
197-
}
198-
199-
impl From<&str> for Error {
200-
fn from(err: &str) -> Self {
201-
Error::Other(err.to_string())
202-
}
203-
}
204-
205-
impl std::error::Error for Error {}
206-
207-
impl core::fmt::Display for Error {
208-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209-
match self {
210-
Error::UrlNotSupported => {
211-
write!(f, "credential provider does not support this registry")
212-
}
213-
Error::ProtocolNotSupported(v) => write!(
214-
f,
215-
"credential provider does not support protocol version {v}"
216-
),
217-
Error::Io(msg) => write!(f, "i/o error: {msg}"),
218-
Error::Serde(msg) => write!(f, "serialization error: {msg}"),
219-
Error::Other(msg) => write!(f, "error: {msg}"),
220-
Error::Subprocess(msg) => write!(f, "subprocess failed: {msg}"),
221-
Error::OperationNotSupported => write!(
222-
f,
223-
"credential provider does not support the requested operation"
224-
),
225-
Error::NotFound => write!(f, "credential not found"),
226-
}
227-
}
228-
}
229-
230168
pub trait Credential {
231169
/// Retrieves a token for the given registry.
232170
fn perform(
@@ -262,7 +200,7 @@ fn doit(credential: impl Credential) -> Result<(), Error> {
262200
}
263201
let request: CredentialRequest = serde_json::from_str(&buffer)?;
264202
if request.v != PROTOCOL_VERSION_1 {
265-
return Err(Error::ProtocolNotSupported(request.v));
203+
return Err(Error::ProtocolNotSupported { version: request.v });
266204
}
267205
serde_json::to_writer(
268206
std::io::stdout(),

0 commit comments

Comments
 (0)