summaryrefslogtreecommitdiffstats
path: root/src/tools/cargo/credential
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 03:59:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-30 03:59:35 +0000
commitd1b2d29528b7794b41e66fc2136e395a02f8529b (patch)
treea4a17504b260206dec3cf55b2dca82929a348ac2 /src/tools/cargo/credential
parentReleasing progress-linux version 1.72.1+dfsg1-1~progress7.99u1. (diff)
downloadrustc-d1b2d29528b7794b41e66fc2136e395a02f8529b.tar.xz
rustc-d1b2d29528b7794b41e66fc2136e395a02f8529b.zip
Merging upstream version 1.73.0+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/cargo/credential')
-rw-r--r--src/tools/cargo/credential/cargo-credential-1password/Cargo.toml6
-rw-r--r--src/tools/cargo/credential/cargo-credential-1password/src/main.rs141
-rw-r--r--src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs8
-rw-r--r--src/tools/cargo/credential/cargo-credential-gnome-secret/src/libsecret.rs190
-rw-r--r--src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs12
-rw-r--r--src/tools/cargo/credential/cargo-credential-libsecret/Cargo.toml (renamed from src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml)13
-rw-r--r--src/tools/cargo/credential/cargo-credential-libsecret/README.md (renamed from src/tools/cargo/credential/cargo-credential-gnome-secret/README.md)2
-rw-r--r--src/tools/cargo/credential/cargo-credential-libsecret/src/lib.rs235
-rw-r--r--src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml6
-rw-r--r--src/tools/cargo/credential/cargo-credential-macos-keychain/src/lib.rs81
-rw-r--r--src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs58
-rw-r--r--src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml6
-rw-r--r--src/tools/cargo/credential/cargo-credential-wincred/src/lib.rs125
-rw-r--r--src/tools/cargo/credential/cargo-credential-wincred/src/main.rs122
-rw-r--r--src/tools/cargo/credential/cargo-credential/Cargo.toml18
-rw-r--r--src/tools/cargo/credential/cargo-credential/README.md2
-rw-r--r--src/tools/cargo/credential/cargo-credential/examples/file-provider.rs90
-rw-r--r--src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs25
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/error.rs206
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/lib.rs312
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/secret.rs101
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/stdio.rs163
-rw-r--r--src/tools/cargo/credential/cargo-credential/tests/examples.rs45
23 files changed, 1412 insertions, 555 deletions
diff --git a/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml b/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml
index 8db40e577..a607e6da1 100644
--- a/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml
+++ b/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "cargo-credential-1password"
-version = "0.2.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
+version = "0.3.0"
+edition.workspace = true
+license.workspace = true
repository = "https://github.com/rust-lang/cargo"
description = "A Cargo credential process that stores tokens in a 1password vault."
diff --git a/src/tools/cargo/credential/cargo-credential-1password/src/main.rs b/src/tools/cargo/credential/cargo-credential-1password/src/main.rs
index 4f512b717..a2607fd2f 100644
--- a/src/tools/cargo/credential/cargo-credential-1password/src/main.rs
+++ b/src/tools/cargo/credential/cargo-credential-1password/src/main.rs
@@ -1,6 +1,8 @@
//! Cargo registry 1password credential process.
-use cargo_credential::{Credential, Error};
+use cargo_credential::{
+ Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo, Secret,
+};
use serde::Deserialize;
use std::io::Read;
use std::process::{Command, Stdio};
@@ -38,13 +40,13 @@ struct Url {
}
impl OnePasswordKeychain {
- fn new() -> Result<OnePasswordKeychain, Error> {
- let mut args = std::env::args().skip(1);
+ fn new(args: &[&str]) -> Result<OnePasswordKeychain, Error> {
+ let mut args = args.iter();
let mut action = false;
let mut account = None;
let mut vault = None;
while let Some(arg) = args.next() {
- match arg.as_str() {
+ match *arg {
"--account" => {
account = Some(args.next().ok_or("--account needs an arg")?);
}
@@ -63,7 +65,10 @@ impl OnePasswordKeychain {
}
}
}
- Ok(OnePasswordKeychain { account, vault })
+ Ok(OnePasswordKeychain {
+ account: account.map(|s| s.to_string()),
+ vault: vault.map(|s| s.to_string()),
+ })
}
fn signin(&self) -> Result<Option<String>, Error> {
@@ -73,9 +78,8 @@ impl OnePasswordKeychain {
return Ok(None);
}
let mut cmd = Command::new("op");
- cmd.args(&["signin", "--raw"]);
+ cmd.args(["signin", "--raw"]);
cmd.stdout(Stdio::piped());
- self.with_tty(&mut cmd)?;
let mut child = cmd
.spawn()
.map_err(|e| format!("failed to spawn `op`: {}", e))?;
@@ -121,19 +125,6 @@ impl OnePasswordKeychain {
cmd
}
- fn with_tty(&self, cmd: &mut Command) -> Result<(), Error> {
- #[cfg(unix)]
- const IN_DEVICE: &str = "/dev/tty";
- #[cfg(windows)]
- const IN_DEVICE: &str = "CONIN$";
- let stdin = std::fs::OpenOptions::new()
- .read(true)
- .write(true)
- .open(IN_DEVICE)?;
- cmd.stdin(stdin);
- Ok(())
- }
-
fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> {
cmd.stdout(Stdio::piped());
let mut child = cmd
@@ -196,12 +187,12 @@ impl OnePasswordKeychain {
&self,
session: &Option<String>,
id: &str,
- token: &str,
+ token: Secret<&str>,
_name: Option<&str>,
) -> Result<(), Error> {
let cmd = self.make_cmd(
session,
- &["item", "edit", id, &format!("password={}", token)],
+ &["item", "edit", id, &format!("password={}", token.expose())],
);
self.run_cmd(cmd)?;
Ok(())
@@ -211,21 +202,21 @@ impl OnePasswordKeychain {
&self,
session: &Option<String>,
index_url: &str,
- token: &str,
+ token: Secret<&str>,
name: Option<&str>,
) -> Result<(), Error> {
let title = match name {
Some(name) => format!("Cargo registry token for {}", name),
None => "Cargo registry token".to_string(),
};
- let mut cmd = self.make_cmd(
+ let cmd = self.make_cmd(
session,
&[
"item",
"create",
"--category",
"Login",
- &format!("password={}", token),
+ &format!("password={}", token.expose()),
&format!("url={}", index_url),
"--title",
&title,
@@ -233,15 +224,11 @@ impl OnePasswordKeychain {
CARGO_TAG,
],
);
- // For unknown reasons, `op item create` seems to not be happy if
- // stdin is not a tty. Otherwise it returns with a 0 exit code without
- // doing anything.
- self.with_tty(&mut cmd)?;
self.run_cmd(cmd)?;
Ok(())
}
- fn get_token(&self, session: &Option<String>, id: &str) -> Result<String, Error> {
+ fn get_token(&self, session: &Option<String>, id: &str) -> Result<Secret<String>, Error> {
let cmd = self.make_cmd(session, &["item", "get", "--format=json", id]);
let buffer = self.run_cmd(cmd)?;
let item: Login = serde_json::from_str(&buffer)
@@ -250,7 +237,8 @@ impl OnePasswordKeychain {
match password {
Some(password) => password
.value
- .ok_or_else(|| format!("missing password value for entry").into()),
+ .map(Secret::from)
+ .ok_or("missing password value for entry".into()),
None => Err("could not find password field".into()),
}
}
@@ -262,53 +250,58 @@ impl OnePasswordKeychain {
}
}
-impl Credential for OnePasswordKeychain {
- fn name(&self) -> &'static str {
- env!("CARGO_PKG_NAME")
- }
-
- fn get(&self, index_url: &str) -> Result<String, Error> {
- let session = self.signin()?;
- if let Some(id) = self.search(&session, index_url)? {
- self.get_token(&session, &id)
- } else {
- return Err(format!(
- "no 1password entry found for registry `{}`, try `cargo login` to add a token",
- index_url
- )
- .into());
- }
- }
-
- fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
- let session = self.signin()?;
- // Check if an item already exists.
- if let Some(id) = self.search(&session, index_url)? {
- self.modify(&session, &id, token, name)
- } else {
- self.create(&session, index_url, token, name)
- }
- }
+pub struct OnePasswordCredential {}
- fn erase(&self, index_url: &str) -> Result<(), Error> {
- let session = self.signin()?;
- // Check if an item already exists.
- if let Some(id) = self.search(&session, index_url)? {
- self.delete(&session, &id)?;
- } else {
- eprintln!("not currently logged in to `{}`", index_url);
+impl Credential for OnePasswordCredential {
+ fn perform(
+ &self,
+ registry: &RegistryInfo,
+ action: &Action,
+ args: &[&str],
+ ) -> Result<CredentialResponse, Error> {
+ let op = OnePasswordKeychain::new(args)?;
+ match action {
+ Action::Get(_) => {
+ let session = op.signin()?;
+ if let Some(id) = op.search(&session, registry.index_url)? {
+ op.get_token(&session, &id)
+ .map(|token| CredentialResponse::Get {
+ token,
+ cache: CacheControl::Session,
+ operation_independent: true,
+ })
+ } else {
+ Err(Error::NotFound)
+ }
+ }
+ Action::Login(options) => {
+ let session = op.signin()?;
+ // Check if an item already exists.
+ if let Some(id) = op.search(&session, registry.index_url)? {
+ eprintln!("note: token already exists for `{}`", registry.index_url);
+ let token = cargo_credential::read_token(options, registry)?;
+ op.modify(&session, &id, token.as_deref(), None)?;
+ } else {
+ let token = cargo_credential::read_token(options, registry)?;
+ op.create(&session, registry.index_url, token.as_deref(), None)?;
+ }
+ Ok(CredentialResponse::Login)
+ }
+ Action::Logout => {
+ let session = op.signin()?;
+ // Check if an item already exists.
+ if let Some(id) = op.search(&session, registry.index_url)? {
+ op.delete(&session, &id)?;
+ Ok(CredentialResponse::Logout)
+ } else {
+ Err(Error::NotFound)
+ }
+ }
+ _ => Err(Error::OperationNotSupported),
}
- Ok(())
}
}
fn main() {
- let op = match OnePasswordKeychain::new() {
- Ok(op) => op,
- Err(e) => {
- eprintln!("error: {}", e);
- std::process::exit(1);
- }
- };
- cargo_credential::main(op);
+ cargo_credential::main(OnePasswordCredential {});
}
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs
deleted file mode 100644
index 8bb86ee43..000000000
--- a/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-fn main() {
- if cfg!(target_os = "linux") {
- // TODO: Consider ignoring errors when libsecret is not installed and
- // switching the impl to UnsupportedCredential (possibly along with a
- // warning?).
- pkg_config::probe_library("libsecret-1").unwrap();
- }
-}
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/src/libsecret.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/src/libsecret.rs
deleted file mode 100644
index c584eeecf..000000000
--- a/src/tools/cargo/credential/cargo-credential-gnome-secret/src/libsecret.rs
+++ /dev/null
@@ -1,190 +0,0 @@
-//! Implementation of the libsecret credential helper.
-
-use cargo_credential::{Credential, Error};
-use std::ffi::{CStr, CString};
-use std::os::raw::{c_char, c_int};
-use std::ptr::{null, null_mut};
-
-#[allow(non_camel_case_types)]
-type gchar = c_char;
-
-#[allow(non_camel_case_types)]
-type gboolean = c_int;
-
-type GQuark = u32;
-
-#[repr(C)]
-struct GError {
- domain: GQuark,
- code: c_int,
- message: *mut gchar,
-}
-
-#[repr(C)]
-struct GCancellable {
- _private: [u8; 0],
-}
-
-#[repr(C)]
-struct SecretSchema {
- name: *const gchar,
- flags: SecretSchemaFlags,
- attributes: [SecretSchemaAttribute; 32],
-}
-
-#[repr(C)]
-#[derive(Copy, Clone)]
-struct SecretSchemaAttribute {
- name: *const gchar,
- attr_type: SecretSchemaAttributeType,
-}
-
-#[repr(C)]
-enum SecretSchemaFlags {
- None = 0,
-}
-
-#[repr(C)]
-#[derive(Copy, Clone)]
-enum SecretSchemaAttributeType {
- String = 0,
-}
-
-extern "C" {
- fn secret_password_store_sync(
- schema: *const SecretSchema,
- collection: *const gchar,
- label: *const gchar,
- password: *const gchar,
- cancellable: *mut GCancellable,
- error: *mut *mut GError,
- ...
- ) -> gboolean;
- fn secret_password_clear_sync(
- schema: *const SecretSchema,
- cancellable: *mut GCancellable,
- error: *mut *mut GError,
- ...
- ) -> gboolean;
- fn secret_password_lookup_sync(
- schema: *const SecretSchema,
- cancellable: *mut GCancellable,
- error: *mut *mut GError,
- ...
- ) -> *mut gchar;
-}
-
-pub struct GnomeSecret;
-
-fn label(index_url: &str) -> CString {
- CString::new(format!("cargo-registry:{}", index_url)).unwrap()
-}
-
-fn schema() -> SecretSchema {
- let mut attributes = [SecretSchemaAttribute {
- name: null(),
- attr_type: SecretSchemaAttributeType::String,
- }; 32];
- attributes[0] = SecretSchemaAttribute {
- name: b"url\0".as_ptr() as *const gchar,
- attr_type: SecretSchemaAttributeType::String,
- };
- SecretSchema {
- name: b"org.rust-lang.cargo.registry\0".as_ptr() as *const gchar,
- flags: SecretSchemaFlags::None,
- attributes,
- }
-}
-
-impl Credential for GnomeSecret {
- fn name(&self) -> &'static str {
- env!("CARGO_PKG_NAME")
- }
-
- fn get(&self, index_url: &str) -> Result<String, Error> {
- let mut error: *mut GError = null_mut();
- let attr_url = CString::new("url").unwrap();
- let index_url_c = CString::new(index_url).unwrap();
- let schema = schema();
- unsafe {
- let token_c = secret_password_lookup_sync(
- &schema,
- null_mut(),
- &mut error,
- attr_url.as_ptr(),
- index_url_c.as_ptr(),
- null() as *const gchar,
- );
- if !error.is_null() {
- return Err(format!(
- "failed to get token: {}",
- CStr::from_ptr((*error).message).to_str()?
- )
- .into());
- }
- if token_c.is_null() {
- return Err(format!("cannot find token for {}", index_url).into());
- }
- let token = CStr::from_ptr(token_c)
- .to_str()
- .map_err(|e| format!("expected utf8 token: {}", e))?
- .to_string();
- Ok(token)
- }
- }
-
- fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
- let label = label(name.unwrap_or(index_url));
- let token = CString::new(token).unwrap();
- let mut error: *mut GError = null_mut();
- let attr_url = CString::new("url").unwrap();
- let index_url_c = CString::new(index_url).unwrap();
- let schema = schema();
- unsafe {
- secret_password_store_sync(
- &schema,
- b"default\0".as_ptr() as *const gchar,
- label.as_ptr(),
- token.as_ptr(),
- null_mut(),
- &mut error,
- attr_url.as_ptr(),
- index_url_c.as_ptr(),
- null() as *const gchar,
- );
- if !error.is_null() {
- return Err(format!(
- "failed to store token: {}",
- CStr::from_ptr((*error).message).to_str()?
- )
- .into());
- }
- }
- Ok(())
- }
-
- fn erase(&self, index_url: &str) -> Result<(), Error> {
- let schema = schema();
- let mut error: *mut GError = null_mut();
- let attr_url = CString::new("url").unwrap();
- let index_url_c = CString::new(index_url).unwrap();
- unsafe {
- secret_password_clear_sync(
- &schema,
- null_mut(),
- &mut error,
- attr_url.as_ptr(),
- index_url_c.as_ptr(),
- null() as *const gchar,
- );
- if !error.is_null() {
- return Err(format!(
- "failed to erase token: {}",
- CStr::from_ptr((*error).message).to_str()?
- )
- .into());
- }
- }
- Ok(())
- }
-}
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs
deleted file mode 100644
index 1d2ecc61f..000000000
--- a/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-//! Cargo registry gnome libsecret credential process.
-
-#[cfg(target_os = "linux")]
-mod libsecret;
-#[cfg(not(target_os = "linux"))]
-use cargo_credential::UnsupportedCredential as GnomeSecret;
-#[cfg(target_os = "linux")]
-use libsecret::GnomeSecret;
-
-fn main() {
- cargo_credential::main(GnomeSecret);
-}
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml b/src/tools/cargo/credential/cargo-credential-libsecret/Cargo.toml
index 63b3e95cc..1bd4bb7d0 100644
--- a/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml
+++ b/src/tools/cargo/credential/cargo-credential-libsecret/Cargo.toml
@@ -1,13 +1,12 @@
[package]
-name = "cargo-credential-gnome-secret"
-version = "0.2.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
+name = "cargo-credential-libsecret"
+version = "0.3.1"
+edition.workspace = true
+license.workspace = true
repository = "https://github.com/rust-lang/cargo"
description = "A Cargo credential process that stores tokens with GNOME libsecret."
[dependencies]
+anyhow.workspace = true
cargo-credential.workspace = true
-
-[build-dependencies]
-pkg-config.workspace = true
+libloading.workspace = true
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/README.md b/src/tools/cargo/credential/cargo-credential-libsecret/README.md
index 7a4b02838..f169323e0 100644
--- a/src/tools/cargo/credential/cargo-credential-gnome-secret/README.md
+++ b/src/tools/cargo/credential/cargo-credential-libsecret/README.md
@@ -1,4 +1,4 @@
-# cargo-credential-gnome-secret
+# cargo-credential-libsecret
This is the implementation for the Cargo credential helper for [GNOME libsecret].
See the [credential-process] documentation for how to use this.
diff --git a/src/tools/cargo/credential/cargo-credential-libsecret/src/lib.rs b/src/tools/cargo/credential/cargo-credential-libsecret/src/lib.rs
new file mode 100644
index 000000000..f83b424ee
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-libsecret/src/lib.rs
@@ -0,0 +1,235 @@
+#[cfg(target_os = "linux")]
+mod linux {
+ //! Implementation of the libsecret credential helper.
+
+ use anyhow::Context;
+ use cargo_credential::{
+ read_token, Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo,
+ Secret,
+ };
+ use libloading::{Library, Symbol};
+ use std::ffi::{CStr, CString};
+ use std::os::raw::{c_char, c_int};
+ use std::ptr::{null, null_mut};
+
+ #[allow(non_camel_case_types)]
+ type gchar = c_char;
+
+ #[allow(non_camel_case_types)]
+ type gboolean = c_int;
+
+ type GQuark = u32;
+
+ #[repr(C)]
+ struct GError {
+ domain: GQuark,
+ code: c_int,
+ message: *mut gchar,
+ }
+
+ #[repr(C)]
+ struct GCancellable {
+ _private: [u8; 0],
+ }
+
+ #[repr(C)]
+ struct SecretSchema {
+ name: *const gchar,
+ flags: SecretSchemaFlags,
+ attributes: [SecretSchemaAttribute; 32],
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct SecretSchemaAttribute {
+ name: *const gchar,
+ attr_type: SecretSchemaAttributeType,
+ }
+
+ #[repr(C)]
+ enum SecretSchemaFlags {
+ None = 0,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ enum SecretSchemaAttributeType {
+ String = 0,
+ }
+
+ type SecretPasswordStoreSync = extern "C" fn(
+ schema: *const SecretSchema,
+ collection: *const gchar,
+ label: *const gchar,
+ password: *const gchar,
+ cancellable: *mut GCancellable,
+ error: *mut *mut GError,
+ ...
+ ) -> gboolean;
+ type SecretPasswordClearSync = extern "C" fn(
+ schema: *const SecretSchema,
+ cancellable: *mut GCancellable,
+ error: *mut *mut GError,
+ ...
+ ) -> gboolean;
+ type SecretPasswordLookupSync = extern "C" fn(
+ schema: *const SecretSchema,
+ cancellable: *mut GCancellable,
+ error: *mut *mut GError,
+ ...
+ ) -> *mut gchar;
+
+ pub struct LibSecretCredential;
+
+ fn label(index_url: &str) -> CString {
+ CString::new(format!("cargo-registry:{}", index_url)).unwrap()
+ }
+
+ fn schema() -> SecretSchema {
+ let mut attributes = [SecretSchemaAttribute {
+ name: null(),
+ attr_type: SecretSchemaAttributeType::String,
+ }; 32];
+ attributes[0] = SecretSchemaAttribute {
+ name: b"url\0".as_ptr() as *const gchar,
+ attr_type: SecretSchemaAttributeType::String,
+ };
+ SecretSchema {
+ name: b"org.rust-lang.cargo.registry\0".as_ptr() as *const gchar,
+ flags: SecretSchemaFlags::None,
+ attributes,
+ }
+ }
+
+ impl Credential for LibSecretCredential {
+ fn perform(
+ &self,
+ registry: &RegistryInfo,
+ action: &Action,
+ _args: &[&str],
+ ) -> Result<CredentialResponse, Error> {
+ // Dynamically load libsecret to avoid users needing to install
+ // additional -dev packages when building this provider.
+ let lib;
+ let secret_password_lookup_sync: Symbol<SecretPasswordLookupSync>;
+ let secret_password_store_sync: Symbol<SecretPasswordStoreSync>;
+ let secret_password_clear_sync: Symbol<SecretPasswordClearSync>;
+ unsafe {
+ lib = Library::new("libsecret-1.so").context(
+ "failed to load libsecret: try installing the `libsecret` \
+ or `libsecret-1-0` package with the system package manager",
+ )?;
+ secret_password_lookup_sync = lib
+ .get(b"secret_password_lookup_sync\0")
+ .map_err(Box::new)?;
+ secret_password_store_sync =
+ lib.get(b"secret_password_store_sync\0").map_err(Box::new)?;
+ secret_password_clear_sync =
+ lib.get(b"secret_password_clear_sync\0").map_err(Box::new)?;
+ }
+
+ let index_url_c = CString::new(registry.index_url).unwrap();
+ match action {
+ cargo_credential::Action::Get(_) => {
+ let mut error: *mut GError = null_mut();
+ let attr_url = CString::new("url").unwrap();
+ let schema = schema();
+ unsafe {
+ let token_c = secret_password_lookup_sync(
+ &schema,
+ null_mut(),
+ &mut error,
+ attr_url.as_ptr(),
+ index_url_c.as_ptr(),
+ null() as *const gchar,
+ );
+ if !error.is_null() {
+ return Err(format!(
+ "failed to get token: {}",
+ CStr::from_ptr((*error).message)
+ .to_str()
+ .unwrap_or_default()
+ )
+ .into());
+ }
+ if token_c.is_null() {
+ return Err(Error::NotFound);
+ }
+ let token = Secret::from(
+ CStr::from_ptr(token_c)
+ .to_str()
+ .map_err(|e| format!("expected utf8 token: {}", e))?
+ .to_string(),
+ );
+ Ok(CredentialResponse::Get {
+ token,
+ cache: CacheControl::Session,
+ operation_independent: true,
+ })
+ }
+ }
+ cargo_credential::Action::Login(options) => {
+ let label = label(registry.name.unwrap_or(registry.index_url));
+ let token = CString::new(read_token(options, registry)?.expose()).unwrap();
+ let mut error: *mut GError = null_mut();
+ let attr_url = CString::new("url").unwrap();
+ let schema = schema();
+ unsafe {
+ secret_password_store_sync(
+ &schema,
+ b"default\0".as_ptr() as *const gchar,
+ label.as_ptr(),
+ token.as_ptr(),
+ null_mut(),
+ &mut error,
+ attr_url.as_ptr(),
+ index_url_c.as_ptr(),
+ null() as *const gchar,
+ );
+ if !error.is_null() {
+ return Err(format!(
+ "failed to store token: {}",
+ CStr::from_ptr((*error).message)
+ .to_str()
+ .unwrap_or_default()
+ )
+ .into());
+ }
+ }
+ Ok(CredentialResponse::Login)
+ }
+ cargo_credential::Action::Logout => {
+ let schema = schema();
+ let mut error: *mut GError = null_mut();
+ let attr_url = CString::new("url").unwrap();
+ unsafe {
+ secret_password_clear_sync(
+ &schema,
+ null_mut(),
+ &mut error,
+ attr_url.as_ptr(),
+ index_url_c.as_ptr(),
+ null() as *const gchar,
+ );
+ if !error.is_null() {
+ return Err(format!(
+ "failed to erase token: {}",
+ CStr::from_ptr((*error).message)
+ .to_str()
+ .unwrap_or_default()
+ )
+ .into());
+ }
+ }
+ Ok(CredentialResponse::Logout)
+ }
+ _ => Err(Error::OperationNotSupported),
+ }
+ }
+ }
+}
+
+#[cfg(not(target_os = "linux"))]
+pub use cargo_credential::UnsupportedCredential as LibSecretCredential;
+#[cfg(target_os = "linux")]
+pub use linux::LibSecretCredential;
diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml b/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml
index 6311b71de..342c771b5 100644
--- a/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml
+++ b/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "cargo-credential-macos-keychain"
-version = "0.2.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
+version = "0.3.0"
+edition.workspace = true
+license.workspace = true
repository = "https://github.com/rust-lang/cargo"
description = "A Cargo credential process that stores tokens in a macOS keychain."
diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/src/lib.rs b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/lib.rs
new file mode 100644
index 000000000..9e6d55472
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/lib.rs
@@ -0,0 +1,81 @@
+//! Cargo registry macos keychain credential process.
+
+#[cfg(target_os = "macos")]
+mod macos {
+ use cargo_credential::{
+ read_token, Action, CacheControl, Credential, CredentialResponse, Error, RegistryInfo,
+ };
+ use security_framework::os::macos::keychain::SecKeychain;
+
+ pub struct MacKeychain;
+
+ /// The account name is not used.
+ const ACCOUNT: &'static str = "";
+ const NOT_FOUND: i32 = -25300; // errSecItemNotFound
+
+ fn registry(index_url: &str) -> String {
+ format!("cargo-registry:{}", index_url)
+ }
+
+ impl Credential for MacKeychain {
+ fn perform(
+ &self,
+ reg: &RegistryInfo<'_>,
+ action: &Action<'_>,
+ _args: &[&str],
+ ) -> Result<CredentialResponse, Error> {
+ let keychain = SecKeychain::default().unwrap();
+ let service_name = registry(reg.index_url);
+ let not_found = security_framework::base::Error::from(NOT_FOUND).code();
+ match action {
+ Action::Get(_) => match keychain.find_generic_password(&service_name, ACCOUNT) {
+ Err(e) if e.code() == not_found => Err(Error::NotFound),
+ Err(e) => Err(Box::new(e).into()),
+ Ok((pass, _)) => {
+ let token = String::from_utf8(pass.as_ref().to_vec()).map_err(Box::new)?;
+ Ok(CredentialResponse::Get {
+ token: token.into(),
+ cache: CacheControl::Session,
+ operation_independent: true,
+ })
+ }
+ },
+ Action::Login(options) => {
+ let token = read_token(options, reg)?;
+ match keychain.find_generic_password(&service_name, ACCOUNT) {
+ Err(e) => {
+ if e.code() == not_found {
+ keychain
+ .add_generic_password(
+ &service_name,
+ ACCOUNT,
+ token.expose().as_bytes(),
+ )
+ .map_err(Box::new)?;
+ }
+ }
+ Ok((_, mut item)) => {
+ item.set_password(token.expose().as_bytes())
+ .map_err(Box::new)?;
+ }
+ }
+ Ok(CredentialResponse::Login)
+ }
+ Action::Logout => match keychain.find_generic_password(&service_name, ACCOUNT) {
+ Err(e) if e.code() == not_found => Err(Error::NotFound),
+ Err(e) => Err(Box::new(e).into()),
+ Ok((_, item)) => {
+ item.delete();
+ Ok(CredentialResponse::Logout)
+ }
+ },
+ _ => Err(Error::OperationNotSupported),
+ }
+ }
+ }
+}
+
+#[cfg(not(target_os = "macos"))]
+pub use cargo_credential::UnsupportedCredential as MacKeychain;
+#[cfg(target_os = "macos")]
+pub use macos::MacKeychain;
diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs
deleted file mode 100644
index 4d6ea96d0..000000000
--- a/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-//! Cargo registry macos keychain credential process.
-
-#[cfg(target_os = "macos")]
-mod macos {
- use cargo_credential::{Credential, Error};
- use security_framework::os::macos::keychain::SecKeychain;
-
- pub(crate) struct MacKeychain;
-
- /// The account name is not used.
- const ACCOUNT: &'static str = "";
-
- fn registry(registry_name: &str) -> String {
- format!("cargo-registry:{}", registry_name)
- }
-
- impl Credential for MacKeychain {
- fn name(&self) -> &'static str {
- env!("CARGO_PKG_NAME")
- }
-
- fn get(&self, index_url: &str) -> Result<String, Error> {
- let keychain = SecKeychain::default().unwrap();
- let service_name = registry(index_url);
- let (pass, _item) = keychain.find_generic_password(&service_name, ACCOUNT)?;
- String::from_utf8(pass.as_ref().to_vec())
- .map_err(|_| "failed to convert token to UTF8".into())
- }
-
- fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
- let keychain = SecKeychain::default().unwrap();
- let service_name = registry(name.unwrap_or(index_url));
- if let Ok((_pass, mut item)) = keychain.find_generic_password(&service_name, ACCOUNT) {
- item.set_password(token.as_bytes())?;
- } else {
- keychain.add_generic_password(&service_name, ACCOUNT, token.as_bytes())?;
- }
- Ok(())
- }
-
- fn erase(&self, index_url: &str) -> Result<(), Error> {
- let keychain = SecKeychain::default().unwrap();
- let service_name = registry(index_url);
- let (_pass, item) = keychain.find_generic_password(&service_name, ACCOUNT)?;
- item.delete();
- Ok(())
- }
- }
-}
-
-#[cfg(not(target_os = "macos"))]
-use cargo_credential::UnsupportedCredential as MacKeychain;
-#[cfg(target_os = "macos")]
-use macos::MacKeychain;
-
-fn main() {
- cargo_credential::main(MacKeychain);
-}
diff --git a/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml b/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml
index cd168a8a3..8c609dc4e 100644
--- a/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml
+++ b/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "cargo-credential-wincred"
-version = "0.2.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
+version = "0.3.0"
+edition.workspace = true
+license.workspace = true
repository = "https://github.com/rust-lang/cargo"
description = "A Cargo credential process that stores tokens with Windows Credential Manager."
diff --git a/src/tools/cargo/credential/cargo-credential-wincred/src/lib.rs b/src/tools/cargo/credential/cargo-credential-wincred/src/lib.rs
new file mode 100644
index 000000000..9200ca58f
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-wincred/src/lib.rs
@@ -0,0 +1,125 @@
+//! Cargo registry windows credential process.
+
+#[cfg(windows)]
+mod win {
+ use cargo_credential::{read_token, Action, CacheControl, CredentialResponse, RegistryInfo};
+ use cargo_credential::{Credential, Error};
+ use std::ffi::OsStr;
+
+ use std::os::windows::ffi::OsStrExt;
+
+ use windows_sys::core::PWSTR;
+ use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
+ use windows_sys::Win32::Foundation::FILETIME;
+ use windows_sys::Win32::Foundation::TRUE;
+ use windows_sys::Win32::Security::Credentials::CredReadW;
+ use windows_sys::Win32::Security::Credentials::CredWriteW;
+ use windows_sys::Win32::Security::Credentials::CREDENTIALW;
+ use windows_sys::Win32::Security::Credentials::CRED_PERSIST_LOCAL_MACHINE;
+ use windows_sys::Win32::Security::Credentials::CRED_TYPE_GENERIC;
+ use windows_sys::Win32::Security::Credentials::{CredDeleteW, CredFree};
+
+ pub struct WindowsCredential;
+
+ /// Converts a string to a nul-terminated wide UTF-16 byte sequence.
+ fn wstr(s: &str) -> Vec<u16> {
+ let mut wide: Vec<u16> = OsStr::new(s).encode_wide().collect();
+ if wide.iter().any(|b| *b == 0) {
+ panic!("nul byte in wide string");
+ }
+ wide.push(0);
+ wide
+ }
+
+ fn target_name(index_url: &str) -> Vec<u16> {
+ wstr(&format!("cargo-registry:{}", index_url))
+ }
+
+ impl Credential for WindowsCredential {
+ fn perform(
+ &self,
+ registry: &RegistryInfo,
+ action: &Action,
+ _args: &[&str],
+ ) -> Result<CredentialResponse, Error> {
+ match action {
+ Action::Get(_) => {
+ let target_name = target_name(registry.index_url);
+ let mut p_credential: *mut CREDENTIALW = std::ptr::null_mut() as *mut _;
+ let bytes = unsafe {
+ if CredReadW(
+ target_name.as_ptr(),
+ CRED_TYPE_GENERIC,
+ 0,
+ &mut p_credential as *mut _,
+ ) != TRUE
+ {
+ let err = std::io::Error::last_os_error();
+ if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) {
+ return Err(Error::NotFound);
+ }
+ return Err(Box::new(err).into());
+ }
+ std::slice::from_raw_parts(
+ (*p_credential).CredentialBlob,
+ (*p_credential).CredentialBlobSize as usize,
+ )
+ };
+ let token = String::from_utf8(bytes.to_vec()).map_err(Box::new);
+ unsafe { CredFree(p_credential as *mut _) };
+ Ok(CredentialResponse::Get {
+ token: token?.into(),
+ cache: CacheControl::Session,
+ operation_independent: true,
+ })
+ }
+ Action::Login(options) => {
+ let token = read_token(options, registry)?.expose();
+ let target_name = target_name(registry.index_url);
+ let comment = wstr("Cargo registry token");
+ let credential = CREDENTIALW {
+ Flags: 0,
+ Type: CRED_TYPE_GENERIC,
+ TargetName: target_name.as_ptr() as PWSTR,
+ Comment: comment.as_ptr() as PWSTR,
+ LastWritten: FILETIME {
+ dwLowDateTime: 0,
+ dwHighDateTime: 0,
+ },
+ CredentialBlobSize: token.len() as u32,
+ CredentialBlob: token.as_bytes().as_ptr() as *mut u8,
+ Persist: CRED_PERSIST_LOCAL_MACHINE,
+ AttributeCount: 0,
+ Attributes: std::ptr::null_mut(),
+ TargetAlias: std::ptr::null_mut(),
+ UserName: std::ptr::null_mut(),
+ };
+ let result = unsafe { CredWriteW(&credential, 0) };
+ if result != TRUE {
+ let err = std::io::Error::last_os_error();
+ return Err(Box::new(err).into());
+ }
+ Ok(CredentialResponse::Login)
+ }
+ Action::Logout => {
+ let target_name = target_name(registry.index_url);
+ let result = unsafe { CredDeleteW(target_name.as_ptr(), CRED_TYPE_GENERIC, 0) };
+ if result != TRUE {
+ let err = std::io::Error::last_os_error();
+ if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) {
+ return Err(Error::NotFound);
+ }
+ return Err(Box::new(err).into());
+ }
+ Ok(CredentialResponse::Logout)
+ }
+ _ => Err(Error::OperationNotSupported),
+ }
+ }
+ }
+}
+
+#[cfg(not(windows))]
+pub use cargo_credential::UnsupportedCredential as WindowsCredential;
+#[cfg(windows)]
+pub use win::WindowsCredential;
diff --git a/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs b/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs
deleted file mode 100644
index 4377172e8..000000000
--- a/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs
+++ /dev/null
@@ -1,122 +0,0 @@
-//! Cargo registry windows credential process.
-
-#[cfg(windows)]
-mod win {
- use cargo_credential::{Credential, Error};
- use std::ffi::OsStr;
- use std::os::windows::ffi::OsStrExt;
-
- use windows_sys::core::PWSTR;
- use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
- use windows_sys::Win32::Foundation::FILETIME;
- use windows_sys::Win32::Foundation::TRUE;
- use windows_sys::Win32::Security::Credentials::CredDeleteW;
- use windows_sys::Win32::Security::Credentials::CredReadW;
- use windows_sys::Win32::Security::Credentials::CredWriteW;
- use windows_sys::Win32::Security::Credentials::CREDENTIALW;
- use windows_sys::Win32::Security::Credentials::CRED_PERSIST_LOCAL_MACHINE;
- use windows_sys::Win32::Security::Credentials::CRED_TYPE_GENERIC;
-
- pub(crate) struct WindowsCredential;
-
- /// Converts a string to a nul-terminated wide UTF-16 byte sequence.
- fn wstr(s: &str) -> Vec<u16> {
- let mut wide: Vec<u16> = OsStr::new(s).encode_wide().collect();
- if wide.iter().any(|b| *b == 0) {
- panic!("nul byte in wide string");
- }
- wide.push(0);
- wide
- }
-
- fn target_name(registry_name: &str) -> Vec<u16> {
- wstr(&format!("cargo-registry:{}", registry_name))
- }
-
- impl Credential for WindowsCredential {
- fn name(&self) -> &'static str {
- env!("CARGO_PKG_NAME")
- }
-
- fn get(&self, index_url: &str) -> Result<String, Error> {
- let target_name = target_name(index_url);
- let p_credential: *mut CREDENTIALW = std::ptr::null_mut() as *mut _;
- unsafe {
- if CredReadW(
- target_name.as_ptr(),
- CRED_TYPE_GENERIC,
- 0,
- p_credential as *mut _ as *mut _,
- ) != TRUE
- {
- return Err(format!(
- "failed to fetch token: {}",
- std::io::Error::last_os_error()
- )
- .into());
- }
- let bytes = std::slice::from_raw_parts(
- (*p_credential).CredentialBlob,
- (*p_credential).CredentialBlobSize as usize,
- );
- String::from_utf8(bytes.to_vec())
- .map_err(|_| "failed to convert token to UTF8".into())
- }
- }
-
- fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
- let token = token.as_bytes();
- let target_name = target_name(index_url);
- let comment = match name {
- Some(name) => wstr(&format!("Cargo registry token for {}", name)),
- None => wstr("Cargo registry token"),
- };
- let mut credential = CREDENTIALW {
- Flags: 0,
- Type: CRED_TYPE_GENERIC,
- TargetName: target_name.as_ptr() as PWSTR,
- Comment: comment.as_ptr() as PWSTR,
- LastWritten: FILETIME {
- dwLowDateTime: 0,
- dwHighDateTime: 0,
- },
- CredentialBlobSize: token.len() as u32,
- CredentialBlob: token.as_ptr() as *mut u8,
- Persist: CRED_PERSIST_LOCAL_MACHINE,
- AttributeCount: 0,
- Attributes: std::ptr::null_mut(),
- TargetAlias: std::ptr::null_mut(),
- UserName: std::ptr::null_mut(),
- };
- let result = unsafe { CredWriteW(&mut credential, 0) };
- if result != TRUE {
- let err = std::io::Error::last_os_error();
- return Err(format!("failed to store token: {}", err).into());
- }
- Ok(())
- }
-
- fn erase(&self, index_url: &str) -> Result<(), Error> {
- let target_name = target_name(index_url);
- let result = unsafe { CredDeleteW(target_name.as_ptr(), CRED_TYPE_GENERIC, 0) };
- if result != TRUE {
- let err = std::io::Error::last_os_error();
- if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) {
- eprintln!("not currently logged in to `{}`", index_url);
- return Ok(());
- }
- return Err(format!("failed to remove token: {}", err).into());
- }
- Ok(())
- }
- }
-}
-
-#[cfg(not(windows))]
-use cargo_credential::UnsupportedCredential as WindowsCredential;
-#[cfg(windows)]
-use win::WindowsCredential;
-
-fn main() {
- cargo_credential::main(WindowsCredential);
-}
diff --git a/src/tools/cargo/credential/cargo-credential/Cargo.toml b/src/tools/cargo/credential/cargo-credential/Cargo.toml
index 2addaf5af..8cd1348be 100644
--- a/src/tools/cargo/credential/cargo-credential/Cargo.toml
+++ b/src/tools/cargo/credential/cargo-credential/Cargo.toml
@@ -1,9 +1,21 @@
[package]
name = "cargo-credential"
-version = "0.2.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
+version = "0.3.0"
+edition.workspace = true
+license.workspace = true
repository = "https://github.com/rust-lang/cargo"
description = "A library to assist writing Cargo credential helpers."
[dependencies]
+anyhow.workspace = true
+libc.workspace = true
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+thiserror.workspace = true
+time.workspace = true
+
+[target.'cfg(windows)'.dependencies]
+windows-sys = { workspace = true, features = ["Win32_System_Console", "Win32_Foundation"] }
+
+[dev-dependencies]
+snapbox = { workspace = true, features = ["examples"] }
diff --git a/src/tools/cargo/credential/cargo-credential/README.md b/src/tools/cargo/credential/cargo-credential/README.md
index 53dc8e6b7..049b3ba55 100644
--- a/src/tools/cargo/credential/cargo-credential/README.md
+++ b/src/tools/cargo/credential/cargo-credential/README.md
@@ -18,7 +18,7 @@ Create a Cargo project with this as a dependency:
# Add this to your Cargo.toml:
[dependencies]
-cargo-credential = "0.1"
+cargo-credential = "0.3"
```
And then include a `main.rs` binary which implements the `Credential` trait, and calls
diff --git a/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs b/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs
new file mode 100644
index 000000000..d11958536
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/examples/file-provider.rs
@@ -0,0 +1,90 @@
+//! Example credential provider that stores credentials in a JSON file.
+//! This is not secure
+
+use cargo_credential::{
+ Action, CacheControl, Credential, CredentialResponse, RegistryInfo, Secret,
+};
+use std::{collections::HashMap, fs::File, io::ErrorKind};
+type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
+
+struct FileCredential;
+
+impl Credential for FileCredential {
+ fn perform(
+ &self,
+ registry: &RegistryInfo,
+ action: &Action,
+ _args: &[&str],
+ ) -> Result<CredentialResponse, cargo_credential::Error> {
+ if registry.index_url != "https://github.com/rust-lang/crates.io-index" {
+ // Restrict this provider to only work for crates.io. Cargo will skip it and attempt
+ // another provider for any other registry.
+ //
+ // If a provider supports any registry, then this check should be omitted.
+ return Err(cargo_credential::Error::UrlNotSupported);
+ }
+
+ // `Error::Other` takes a boxed `std::error::Error` type that causes Cargo to show the error.
+ let mut creds = FileCredential::read().map_err(cargo_credential::Error::Other)?;
+
+ match action {
+ Action::Get(_) => {
+ // Cargo requested a token, look it up.
+ if let Some(token) = creds.get(registry.index_url) {
+ Ok(CredentialResponse::Get {
+ token: token.clone(),
+ cache: CacheControl::Session,
+ operation_independent: true,
+ })
+ } else {
+ // Credential providers should respond with `NotFound` when a credential can not be
+ // found, allowing Cargo to attempt another provider.
+ Err(cargo_credential::Error::NotFound)
+ }
+ }
+ Action::Login(login_options) => {
+ // The token for `cargo login` can come from the `login_options` parameter or i
+ // interactively reading from stdin.
+ //
+ // `cargo_credential::read_token` automatically handles this.
+ let token = cargo_credential::read_token(login_options, registry)?;
+ creds.insert(registry.index_url.to_string(), token);
+
+ FileCredential::write(&creds).map_err(cargo_credential::Error::Other)?;
+
+ // Credentials were successfully stored.
+ Ok(CredentialResponse::Login)
+ }
+ Action::Logout => {
+ if creds.remove(registry.index_url).is_none() {
+ // If the user attempts to log out from a registry that has no credentials
+ // stored, then NotFound is the appropriate error.
+ Err(cargo_credential::Error::NotFound)
+ } else {
+ // Credentials were successfully erased.
+ Ok(CredentialResponse::Logout)
+ }
+ }
+ // If a credential provider doesn't support a given operation, it should respond with `OperationNotSupported`.
+ _ => Err(cargo_credential::Error::OperationNotSupported),
+ }
+ }
+}
+
+impl FileCredential {
+ fn read() -> Result<HashMap<String, Secret<String>>, Error> {
+ match File::open("cargo-credentials.json") {
+ Ok(f) => Ok(serde_json::from_reader(f)?),
+ Err(e) if e.kind() == ErrorKind::NotFound => Ok(HashMap::new()),
+ Err(e) => Err(e)?,
+ }
+ }
+ fn write(value: &HashMap<String, Secret<String>>) -> Result<(), Error> {
+ let file = File::create("cargo-credentials.json")?;
+ Ok(serde_json::to_writer_pretty(file, value)?)
+ }
+}
+
+fn main() {
+ cargo_credential::main(FileCredential);
+}
diff --git a/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs b/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs
new file mode 100644
index 000000000..0b9bcc2f7
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/examples/stdout-redirected.rs
@@ -0,0 +1,25 @@
+//! Provider used for testing redirection of stdout.
+
+use cargo_credential::{Action, Credential, CredentialResponse, Error, RegistryInfo};
+
+struct MyCredential;
+
+impl Credential for MyCredential {
+ fn perform(
+ &self,
+ _registry: &RegistryInfo,
+ _action: &Action,
+ _args: &[&str],
+ ) -> Result<CredentialResponse, Error> {
+ // Informational messages should be sent on stderr.
+ eprintln!("message on stderr should be sent the the parent process");
+
+ // Reading from stdin and writing to stdout will go to the attached console (tty).
+ println!("message from test credential provider");
+ Err(Error::OperationNotSupported)
+ }
+}
+
+fn main() {
+ cargo_credential::main(MyCredential);
+}
diff --git a/src/tools/cargo/credential/cargo-credential/src/error.rs b/src/tools/cargo/credential/cargo-credential/src/error.rs
new file mode 100644
index 000000000..2ebaf9977
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/src/error.rs
@@ -0,0 +1,206 @@
+use serde::{Deserialize, Serialize};
+use std::error::Error as StdError;
+use thiserror::Error as ThisError;
+
+/// Credential provider error type.
+///
+/// `UrlNotSupported` and `NotFound` errors both cause Cargo
+/// to attempt another provider, if one is available. The other
+/// variants are fatal.
+///
+/// Note: Do not add a tuple variant, as it cannot be serialized.
+#[derive(Serialize, Deserialize, ThisError, Debug)]
+#[serde(rename_all = "kebab-case", tag = "kind")]
+#[non_exhaustive]
+pub enum Error {
+ /// Registry URL is not supported. This should be used if
+ /// the provider only works for some registries. Cargo will
+ /// try another provider, if available
+ #[error("registry not supported")]
+ UrlNotSupported,
+
+ /// Credentials could not be found. Cargo will try another
+ /// provider, if available
+ #[error("credential not found")]
+ NotFound,
+
+ /// The provider doesn't support this operation, such as
+ /// a provider that can't support 'login' / 'logout'
+ #[error("requested operation not supported")]
+ OperationNotSupported,
+
+ /// The provider failed to perform the operation. Other
+ /// providers will not be attempted
+ #[error(transparent)]
+ #[serde(with = "error_serialize")]
+ Other(Box<dyn StdError + Sync + Send>),
+
+ /// A new variant was added to this enum since Cargo was built
+ #[error("unknown error kind; try updating Cargo?")]
+ #[serde(other)]
+ Unknown,
+}
+
+impl From<String> for Error {
+ fn from(err: String) -> Self {
+ Box::new(StringTypedError {
+ message: err.to_string(),
+ source: None,
+ })
+ .into()
+ }
+}
+
+impl From<&str> for Error {
+ fn from(err: &str) -> Self {
+ err.to_string().into()
+ }
+}
+
+impl From<anyhow::Error> for Error {
+ fn from(value: anyhow::Error) -> Self {
+ let mut prev = None;
+ for e in value.chain().rev() {
+ prev = Some(Box::new(StringTypedError {
+ message: e.to_string(),
+ source: prev,
+ }));
+ }
+ Error::Other(prev.unwrap())
+ }
+}
+
+impl<T: StdError + Send + Sync + 'static> From<Box<T>> for Error {
+ fn from(value: Box<T>) -> Self {
+ Error::Other(value)
+ }
+}
+
+/// String-based error type with an optional source
+#[derive(Debug)]
+struct StringTypedError {
+ message: String,
+ source: Option<Box<StringTypedError>>,
+}
+
+impl StdError for StringTypedError {
+ fn source(&self) -> Option<&(dyn StdError + 'static)> {
+ self.source.as_ref().map(|err| err as &dyn StdError)
+ }
+}
+
+impl std::fmt::Display for StringTypedError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.message.fmt(f)
+ }
+}
+
+/// Serializer / deserializer for any boxed error.
+/// The string representation of the error, and its `source` chain can roundtrip across
+/// the serialization. The actual types are lost (downcast will not work).
+mod error_serialize {
+ use std::error::Error as StdError;
+ use std::ops::Deref;
+
+ use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serializer};
+
+ use crate::error::StringTypedError;
+
+ pub fn serialize<S>(
+ e: &Box<dyn StdError + Send + Sync>,
+ serializer: S,
+ ) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut state = serializer.serialize_struct("StringTypedError", 2)?;
+ state.serialize_field("message", &format!("{}", e))?;
+
+ // Serialize the source error chain recursively
+ let mut current_source: &dyn StdError = e.deref();
+ let mut sources = Vec::new();
+ while let Some(err) = current_source.source() {
+ sources.push(err.to_string());
+ current_source = err;
+ }
+ state.serialize_field("caused-by", &sources)?;
+ state.end()
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<dyn StdError + Sync + Send>, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Deserialize)]
+ #[serde(rename_all = "kebab-case")]
+ struct ErrorData {
+ message: String,
+ caused_by: Option<Vec<String>>,
+ }
+ let data = ErrorData::deserialize(deserializer)?;
+ let mut prev = None;
+ if let Some(source) = data.caused_by {
+ for e in source.into_iter().rev() {
+ prev = Some(Box::new(StringTypedError {
+ message: e,
+ source: prev,
+ }));
+ }
+ }
+ let e = Box::new(StringTypedError {
+ message: data.message,
+ source: prev,
+ });
+ Ok(e)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Error;
+
+ #[test]
+ pub fn unknown_kind() {
+ let json = r#"{
+ "kind": "unexpected-kind",
+ "unexpected-content": "test"
+ }"#;
+ let e: Error = serde_json::from_str(&json).unwrap();
+ assert!(matches!(e, Error::Unknown));
+ }
+
+ #[test]
+ pub fn roundtrip() {
+ // Construct an error with context
+ let e = anyhow::anyhow!("E1").context("E2").context("E3");
+ // Convert to a string with contexts.
+ let s1 = format!("{:?}", e);
+ // Convert the error into an `Error`
+ let e: Error = e.into();
+ // Convert that error into JSON
+ let json = serde_json::to_string_pretty(&e).unwrap();
+ // Convert that error back to anyhow
+ let e: anyhow::Error = e.into();
+ let s2 = format!("{:?}", e);
+ assert_eq!(s1, s2);
+
+ // Convert the error back from JSON
+ let e: Error = serde_json::from_str(&json).unwrap();
+ // Convert to back to anyhow
+ let e: anyhow::Error = e.into();
+ let s3 = format!("{:?}", e);
+ assert_eq!(s2, s3);
+
+ assert_eq!(
+ r#"{
+ "kind": "other",
+ "message": "E3",
+ "caused-by": [
+ "E2",
+ "E1"
+ ]
+}"#,
+ json
+ );
+ }
+}
diff --git a/src/tools/cargo/credential/cargo-credential/src/lib.rs b/src/tools/cargo/credential/cargo-credential/src/lib.rs
index c75172242..0fb495ed3 100644
--- a/src/tools/cargo/credential/cargo-credential/src/lib.rs
+++ b/src/tools/cargo/credential/cargo-credential/src/lib.rs
@@ -1,4 +1,4 @@
-//! Helper library for writing Cargo credential processes.
+//! Helper library for writing Cargo credential providers.
//!
//! A credential process should have a `struct` that implements the `Credential` trait.
//! The `main` function should be called with an instance of that struct, such as:
@@ -9,98 +9,270 @@
//! }
//! ```
//!
-//! This will determine the action to perform (get/store/erase) by looking at
-//! the CLI arguments for the first argument that does not start with `-`. It
-//! will then call the corresponding method of the trait to perform the
-//! requested action.
-
-pub type Error = Box<dyn std::error::Error>;
+//! While in the `perform` function, stdin and stdout will be re-attached to the
+//! active console. This allows credential providers to be interactive if necessary.
+//!
+//! ## Error handling
+//! ### [`Error::UrlNotSupported`]
+//! A credential provider may only support some registry URLs. If this is the case
+//! and an unsupported index URL is passed to the provider, it should respond with
+//! [`Error::UrlNotSupported`]. Other credential providers may be attempted by Cargo.
+//!
+//! ### [`Error::NotFound`]
+//! When attempting an [`Action::Get`] or [`Action::Logout`], if a credential can not
+//! be found, the provider should respond with [`Error::NotFound`]. Other credential
+//! providers may be attempted by Cargo.
+//!
+//! ### [`Error::OperationNotSupported`]
+//! A credential provider might not support all operations. For example if the provider
+//! only supports [`Action::Get`], [`Error::OperationNotSupported`] should be returned
+//! for all other requests.
+//!
+//! ### [`Error::Other`]
+//! All other errors go here. The error will be shown to the user in Cargo, including
+//! the full error chain using [`std::error::Error::source`].
+//!
+//! ## Example
+//! ```rust,ignore
+#![doc = include_str!("../examples/file-provider.rs")]
+//! ```
-pub trait Credential {
- /// Returns the name of this credential process.
- fn name(&self) -> &'static str;
+use serde::{Deserialize, Serialize};
+use std::{fmt::Display, io};
+use time::OffsetDateTime;
- /// Retrieves a token for the given registry.
- fn get(&self, index_url: &str) -> Result<String, Error>;
+mod error;
+mod secret;
+mod stdio;
- /// Stores the given token for the given registry.
- fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error>;
+pub use error::Error;
+pub use secret::Secret;
+use stdio::stdin_stdout_to_console;
- /// Removes the token for the given registry.
- ///
- /// If the user is not logged in, this should print a message to stderr if
- /// possible indicating that the user is not currently logged in, and
- /// return `Ok`.
- fn erase(&self, index_url: &str) -> Result<(), Error>;
+/// Message sent by the credential helper on startup
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct CredentialHello {
+ // Protocol versions supported by the credential process.
+ pub v: Vec<u32>,
}
+/// Credential provider that doesn't support any registries.
pub struct UnsupportedCredential;
-
impl Credential for UnsupportedCredential {
- fn name(&self) -> &'static str {
- "unsupported"
+ fn perform(
+ &self,
+ _registry: &RegistryInfo,
+ _action: &Action,
+ _args: &[&str],
+ ) -> Result<CredentialResponse, Error> {
+ Err(Error::UrlNotSupported)
}
+}
- fn get(&self, _index_url: &str) -> Result<String, Error> {
- Err("unsupported".into())
- }
+/// Message sent by Cargo to the credential helper after the hello
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub struct CredentialRequest<'a> {
+ // Cargo will respond with the highest common protocol supported by both.
+ pub v: u32,
+ #[serde(borrow)]
+ pub registry: RegistryInfo<'a>,
+ #[serde(borrow, flatten)]
+ pub action: Action<'a>,
+ /// Additional command-line arguments passed to the credential provider.
+ pub args: Vec<&'a str>,
+}
- fn store(&self, _index_url: &str, _token: &str, _name: Option<&str>) -> Result<(), Error> {
- Err("unsupported".into())
- }
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub struct RegistryInfo<'a> {
+ /// Registry index url
+ pub index_url: &'a str,
+ /// Name of the registry in configuration. May not be available.
+ /// The crates.io registry will be `crates-io` (`CRATES_IO_REGISTRY`).
+ pub name: Option<&'a str>,
+ /// Headers from attempting to access a registry that resulted in a HTTP 401.
+ #[serde(skip_serializing_if = "Vec::is_empty", default)]
+ pub headers: Vec<String>,
+}
- fn erase(&self, _index_url: &str) -> Result<(), Error> {
- Err("unsupported".into())
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[non_exhaustive]
+#[serde(tag = "kind", rename_all = "kebab-case")]
+pub enum Action<'a> {
+ #[serde(borrow)]
+ Get(Operation<'a>),
+ Login(LoginOptions<'a>),
+ Logout,
+ #[serde(other)]
+ Unknown,
+}
+
+impl<'a> Display for Action<'a> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Action::Get(_) => f.write_str("get"),
+ Action::Login(_) => f.write_str("login"),
+ Action::Logout => f.write_str("logout"),
+ Action::Unknown => f.write_str("<unknown>"),
+ }
}
}
-/// Runs the credential interaction by processing the command-line and
-/// environment variables.
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "kebab-case")]
+pub struct LoginOptions<'a> {
+ /// Token passed on the command line via --token or from stdin
+ pub token: Option<Secret<&'a str>>,
+ /// Optional URL that the user can visit to log in to the registry
+ pub login_url: Option<&'a str>,
+}
+
+/// A record of what kind of operation is happening that we should generate a token for.
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[non_exhaustive]
+#[serde(tag = "operation", rename_all = "kebab-case")]
+pub enum Operation<'a> {
+ /// The user is attempting to fetch a crate.
+ Read,
+ /// The user is attempting to publish a crate.
+ Publish {
+ /// The name of the crate
+ name: &'a str,
+ /// The version of the crate
+ vers: &'a str,
+ /// The checksum of the crate file being uploaded
+ cksum: &'a str,
+ },
+ /// The user is attempting to yank a crate.
+ Yank {
+ /// The name of the crate
+ name: &'a str,
+ /// The version of the crate
+ vers: &'a str,
+ },
+ /// The user is attempting to unyank a crate.
+ Unyank {
+ /// The name of the crate
+ name: &'a str,
+ /// The version of the crate
+ vers: &'a str,
+ },
+ /// The user is attempting to modify the owners of a crate.
+ Owners {
+ /// The name of the crate
+ name: &'a str,
+ },
+ #[serde(other)]
+ Unknown,
+}
+
+/// Message sent by the credential helper
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "kind", rename_all = "kebab-case")]
+#[non_exhaustive]
+pub enum CredentialResponse {
+ Get {
+ token: Secret<String>,
+ cache: CacheControl,
+ operation_independent: bool,
+ },
+ Login,
+ Logout,
+ #[serde(other)]
+ Unknown,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(rename_all = "kebab-case")]
+#[non_exhaustive]
+pub enum CacheControl {
+ /// Do not cache this result.
+ Never,
+ /// Cache this result and use it for subsequent requests in the current Cargo invocation until the specified time.
+ Expires(#[serde(with = "time::serde::timestamp")] OffsetDateTime),
+ /// Cache this result and use it for all subsequent requests in the current Cargo invocation.
+ Session,
+ #[serde(other)]
+ Unknown,
+}
+
+/// Credential process JSON protocol version. Incrementing
+/// this version will prevent new credential providers
+/// from working with older versions of Cargo.
+pub const PROTOCOL_VERSION_1: u32 = 1;
+pub trait Credential {
+ /// Retrieves a token for the given registry.
+ fn perform(
+ &self,
+ registry: &RegistryInfo,
+ action: &Action,
+ args: &[&str],
+ ) -> Result<CredentialResponse, Error>;
+}
+
+/// Runs the credential interaction
pub fn main(credential: impl Credential) {
- let name = credential.name();
- if let Err(e) = doit(credential) {
- eprintln!("{} error: {}", name, e);
- std::process::exit(1);
+ let result = doit(credential).map_err(|e| Error::Other(e));
+ if result.is_err() {
+ serde_json::to_writer(std::io::stdout(), &result)
+ .expect("failed to serialize credential provider error");
+ println!();
}
}
-fn env(name: &str) -> Result<String, Error> {
- std::env::var(name).map_err(|_| format!("environment variable `{}` is not set", name).into())
-}
+fn doit(
+ credential: impl Credential,
+) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ let hello = CredentialHello {
+ v: vec![PROTOCOL_VERSION_1],
+ };
+ serde_json::to_writer(std::io::stdout(), &hello)?;
+ println!();
-fn doit(credential: impl Credential) -> Result<(), Error> {
- let which = std::env::args()
- .skip(1)
- .skip_while(|arg| arg.starts_with('-'))
- .next()
- .ok_or_else(|| "first argument must be the {action}")?;
- let index_url = env("CARGO_REGISTRY_INDEX_URL")?;
- let name = std::env::var("CARGO_REGISTRY_NAME_OPT").ok();
- let result = match which.as_ref() {
- "get" => credential.get(&index_url).and_then(|token| {
- println!("{}", token);
- Ok(())
- }),
- "store" => {
- read_token().and_then(|token| credential.store(&index_url, &token, name.as_deref()))
+ loop {
+ let mut buffer = String::new();
+ let len = std::io::stdin().read_line(&mut buffer)?;
+ if len == 0 {
+ return Ok(());
}
- "erase" => credential.erase(&index_url),
- _ => {
- return Err(format!(
- "unexpected command-line argument `{}`, expected get/store/erase",
- which
- )
- .into())
+ let request: CredentialRequest = serde_json::from_str(&buffer)?;
+ if request.v != PROTOCOL_VERSION_1 {
+ return Err(format!("unsupported protocol version {}", request.v).into());
}
- };
- result.map_err(|e| format!("failed to `{}` token: {}", which, e).into())
+
+ let response = stdin_stdout_to_console(|| {
+ credential.perform(&request.registry, &request.action, &request.args)
+ })?;
+
+ serde_json::to_writer(std::io::stdout(), &response)?;
+ println!();
+ }
}
-fn read_token() -> Result<String, Error> {
- let mut buffer = String::new();
- std::io::stdin().read_line(&mut buffer)?;
- if buffer.ends_with('\n') {
- buffer.pop();
+/// Read a line of text from stdin.
+pub fn read_line() -> Result<String, io::Error> {
+ let mut buf = String::new();
+ io::stdin().read_line(&mut buf)?;
+ Ok(buf.trim().to_string())
+}
+
+/// Prompt the user for a token.
+pub fn read_token(
+ login_options: &LoginOptions,
+ registry: &RegistryInfo,
+) -> Result<Secret<String>, Error> {
+ if let Some(token) = &login_options.token {
+ return Ok(token.to_owned());
}
- Ok(buffer)
+
+ if let Some(url) = login_options.login_url {
+ eprintln!("please paste the token found on {url} below");
+ } else if let Some(name) = registry.name {
+ eprintln!("please paste the token for {name} below");
+ } else {
+ eprintln!("please paste the token for {} below", registry.index_url);
+ }
+
+ Ok(Secret::from(read_line().map_err(Box::new)?))
}
diff --git a/src/tools/cargo/credential/cargo-credential/src/secret.rs b/src/tools/cargo/credential/cargo-credential/src/secret.rs
new file mode 100644
index 000000000..1c2314d8e
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/src/secret.rs
@@ -0,0 +1,101 @@
+use std::fmt;
+use std::ops::Deref;
+
+use serde::{Deserialize, Serialize};
+
+/// A wrapper for values that should not be printed.
+///
+/// This type does not implement `Display`, and has a `Debug` impl that hides
+/// the contained value.
+///
+/// ```
+/// # use cargo_credential::Secret;
+/// let token = Secret::from("super secret string");
+/// assert_eq!(format!("{:?}", token), "Secret { inner: \"REDACTED\" }");
+/// ```
+///
+/// Currently, we write a borrowed `Secret<T>` as `Secret<&T>`.
+/// The [`as_deref`](Secret::as_deref) and [`to_owned`](Secret::to_owned) methods can
+/// be used to convert back and forth between `Secret<String>` and `Secret<&str>`.
+#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Secret<T> {
+ inner: T,
+}
+
+impl<T> Secret<T> {
+ /// Unwraps the contained value.
+ ///
+ /// Use of this method marks the boundary of where the contained value is
+ /// hidden.
+ pub fn expose(self) -> T {
+ self.inner
+ }
+
+ /// Converts a `Secret<T>` to a `Secret<&T::Target>`.
+ /// ```
+ /// # use cargo_credential::Secret;
+ /// let owned: Secret<String> = Secret::from(String::from("token"));
+ /// let borrowed: Secret<&str> = owned.as_deref();
+ /// ```
+ pub fn as_deref(&self) -> Secret<&<T as Deref>::Target>
+ where
+ T: Deref,
+ {
+ Secret::from(self.inner.deref())
+ }
+
+ /// Converts a `Secret<T>` to a `Secret<&T>`.
+ pub fn as_ref(&self) -> Secret<&T> {
+ Secret::from(&self.inner)
+ }
+
+ /// Converts a `Secret<T>` to a `Secret<U>` by applying `f` to the contained value.
+ pub fn map<U, F>(self, f: F) -> Secret<U>
+ where
+ F: FnOnce(T) -> U,
+ {
+ Secret::from(f(self.inner))
+ }
+}
+
+impl<T: ToOwned + ?Sized> Secret<&T> {
+ /// Converts a `Secret` containing a borrowed type to a `Secret` containing the
+ /// corresponding owned type.
+ /// ```
+ /// # use cargo_credential::Secret;
+ /// let borrowed: Secret<&str> = Secret::from("token");
+ /// let owned: Secret<String> = borrowed.to_owned();
+ /// ```
+ pub fn to_owned(&self) -> Secret<<T as ToOwned>::Owned> {
+ Secret::from(self.inner.to_owned())
+ }
+}
+
+impl<T, E> Secret<Result<T, E>> {
+ /// Converts a `Secret<Result<T, E>>` to a `Result<Secret<T>, E>`.
+ pub fn transpose(self) -> Result<Secret<T>, E> {
+ self.inner.map(|v| Secret::from(v))
+ }
+}
+
+impl<T: AsRef<str>> Secret<T> {
+ /// Checks if the contained value is empty.
+ pub fn is_empty(&self) -> bool {
+ self.inner.as_ref().is_empty()
+ }
+}
+
+impl<T> From<T> for Secret<T> {
+ fn from(inner: T) -> Self {
+ Self { inner }
+ }
+}
+
+impl<T> fmt::Debug for Secret<T> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("Secret")
+ .field("inner", &"REDACTED")
+ .finish()
+ }
+}
diff --git a/src/tools/cargo/credential/cargo-credential/src/stdio.rs b/src/tools/cargo/credential/cargo-credential/src/stdio.rs
new file mode 100644
index 000000000..25435056f
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/src/stdio.rs
@@ -0,0 +1,163 @@
+use std::{fs::File, io::Error};
+
+/// Reset stdin and stdout to the attached console / tty for the duration of the closure.
+/// If no console is available, stdin and stdout will be redirected to null.
+pub fn stdin_stdout_to_console<F, T>(f: F) -> Result<T, Error>
+where
+ F: FnOnce() -> T,
+{
+ let open_write = |f| std::fs::OpenOptions::new().write(true).open(f);
+
+ let mut stdin = File::open(imp::IN_DEVICE).or_else(|_| File::open(imp::NULL_DEVICE))?;
+ let mut stdout = open_write(imp::OUT_DEVICE).or_else(|_| open_write(imp::NULL_DEVICE))?;
+
+ let _stdin_guard = imp::ReplacementGuard::new(Stdio::Stdin, &mut stdin)?;
+ let _stdout_guard = imp::ReplacementGuard::new(Stdio::Stdout, &mut stdout)?;
+ Ok(f())
+}
+
+enum Stdio {
+ Stdin,
+ Stdout,
+}
+
+#[cfg(windows)]
+mod imp {
+ use super::Stdio;
+ use std::{fs::File, io::Error, os::windows::prelude::AsRawHandle};
+ use windows_sys::Win32::{
+ Foundation::{HANDLE, INVALID_HANDLE_VALUE},
+ System::Console::{
+ GetStdHandle, SetStdHandle, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
+ },
+ };
+ pub const OUT_DEVICE: &str = "CONOUT$";
+ pub const IN_DEVICE: &str = "CONIN$";
+ pub const NULL_DEVICE: &str = "NUL";
+
+ /// Restores previous stdio when dropped.
+ pub struct ReplacementGuard {
+ std_handle: STD_HANDLE,
+ previous: HANDLE,
+ }
+
+ impl ReplacementGuard {
+ pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> {
+ let std_handle = match stdio {
+ Stdio::Stdin => STD_INPUT_HANDLE,
+ Stdio::Stdout => STD_OUTPUT_HANDLE,
+ };
+
+ let previous;
+ unsafe {
+ // Make a copy of the current handle
+ previous = GetStdHandle(std_handle);
+ if previous == INVALID_HANDLE_VALUE {
+ return Err(std::io::Error::last_os_error());
+ }
+
+ // Replace stdin with the replacement handle
+ if SetStdHandle(std_handle, replacement.as_raw_handle() as HANDLE) == 0 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+
+ Ok(ReplacementGuard {
+ previous,
+ std_handle,
+ })
+ }
+ }
+
+ impl Drop for ReplacementGuard {
+ fn drop(&mut self) {
+ unsafe {
+ // Put previous handle back in to stdin
+ SetStdHandle(self.std_handle, self.previous);
+ }
+ }
+ }
+}
+
+#[cfg(unix)]
+mod imp {
+ use super::Stdio;
+ use libc::{close, dup, dup2, STDIN_FILENO, STDOUT_FILENO};
+ use std::{fs::File, io::Error, os::fd::AsRawFd};
+ pub const IN_DEVICE: &str = "/dev/tty";
+ pub const OUT_DEVICE: &str = "/dev/tty";
+ pub const NULL_DEVICE: &str = "/dev/null";
+
+ /// Restores previous stdio when dropped.
+ pub struct ReplacementGuard {
+ std_fileno: i32,
+ previous: i32,
+ }
+
+ impl ReplacementGuard {
+ pub(super) fn new(stdio: Stdio, replacement: &mut File) -> Result<ReplacementGuard, Error> {
+ let std_fileno = match stdio {
+ Stdio::Stdin => STDIN_FILENO,
+ Stdio::Stdout => STDOUT_FILENO,
+ };
+
+ let previous;
+ unsafe {
+ // Duplicate the existing stdin file to a new descriptor
+ previous = dup(std_fileno);
+ if previous == -1 {
+ return Err(std::io::Error::last_os_error());
+ }
+ // Replace stdin with the replacement file
+ if dup2(replacement.as_raw_fd(), std_fileno) == -1 {
+ return Err(std::io::Error::last_os_error());
+ }
+ }
+
+ Ok(ReplacementGuard {
+ previous,
+ std_fileno,
+ })
+ }
+ }
+
+ impl Drop for ReplacementGuard {
+ fn drop(&mut self) {
+ unsafe {
+ // Put previous file back in to stdin
+ dup2(self.previous, self.std_fileno);
+ // Close the file descriptor we used as a backup
+ close(self.previous);
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::fs::OpenOptions;
+ use std::io::{Seek, Write};
+
+ use super::imp::ReplacementGuard;
+ use super::Stdio;
+
+ #[test]
+ fn stdin() {
+ let tempdir = snapbox::path::PathFixture::mutable_temp().unwrap();
+ let file = tempdir.path().unwrap().join("stdin");
+ let mut file = OpenOptions::new()
+ .read(true)
+ .write(true)
+ .create(true)
+ .open(file)
+ .unwrap();
+
+ writeln!(&mut file, "hello").unwrap();
+ file.seek(std::io::SeekFrom::Start(0)).unwrap();
+ {
+ let _guard = ReplacementGuard::new(Stdio::Stdin, &mut file).unwrap();
+ let line = std::io::stdin().lines().next().unwrap().unwrap();
+ assert_eq!(line, "hello");
+ }
+ }
+}
diff --git a/src/tools/cargo/credential/cargo-credential/tests/examples.rs b/src/tools/cargo/credential/cargo-credential/tests/examples.rs
new file mode 100644
index 000000000..87fdb8de3
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/tests/examples.rs
@@ -0,0 +1,45 @@
+use std::path::Path;
+
+use snapbox::cmd::Command;
+
+#[test]
+fn stdout_redirected() {
+ let bin = snapbox::cmd::compile_example("stdout-redirected", []).unwrap();
+
+ let hello = r#"{"v":[1]}"#;
+ let get_request = r#"{"v": 1, "registry": {"index-url":"sparse+https://test/","name":"alternative"},"kind": "get","operation": "read","args": []}"#;
+ let err_not_supported = r#"{"Err":{"kind":"operation-not-supported"}}"#;
+
+ Command::new(bin)
+ .stdin(format!("{get_request}\n"))
+ .arg("--cargo-plugin")
+ .assert()
+ .stdout_eq(format!("{hello}\n{err_not_supported}\n"))
+ .stderr_eq("message on stderr should be sent the the parent process\n")
+ .success();
+}
+
+#[test]
+fn file_provider() {
+ let bin = snapbox::cmd::compile_example("file-provider", []).unwrap();
+
+ let hello = r#"{"v":[1]}"#;
+ let login_request = r#"{"v": 1,"registry": {"index-url":"https://github.com/rust-lang/crates.io-index","name":"crates-io"},"kind": "login","token": "s3krit","args": []}"#;
+ let login_response = r#"{"Ok":{"kind":"login"}}"#;
+
+ let get_request = r#"{"v": 1,"registry": {"index-url":"https://github.com/rust-lang/crates.io-index","name":"crates-io"},"kind": "get","operation": "read","args": []}"#;
+ let get_response =
+ r#"{"Ok":{"kind":"get","token":"s3krit","cache":"session","operation_independent":true}}"#;
+
+ let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cargo-credential-tests");
+ std::fs::create_dir(&dir).unwrap();
+ Command::new(bin)
+ .current_dir(&dir)
+ .stdin(format!("{login_request}\n{get_request}\n"))
+ .arg("--cargo-plugin")
+ .assert()
+ .stdout_eq(format!("{hello}\n{login_response}\n{get_response}\n"))
+ .stderr_eq("")
+ .success();
+ std::fs::remove_dir_all(&dir).unwrap();
+}