pub use credential::{Credential, CredentialBuilder};
pub use error::{Error, Result};
#[cfg(target_os = "linux")]
pub mod keyutils;
#[cfg(all(target_os = "linux", not(feature = "linux-no-secret-service")))]
pub mod secret_service;
#[cfg(all(target_os = "linux", not(feature = "linux-default-keyutils")))]
use crate::secret_service as default;
#[cfg(all(target_os = "linux", feature = "linux-default-keyutils"))]
use keyutils as default;
#[cfg(target_os = "windows")]
pub mod windows;
#[cfg(target_os = "windows")]
use windows as default;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "macos")]
use macos as default;
#[cfg(target_os = "ios")]
pub mod ios;
#[cfg(target_os = "ios")]
use ios as default;
pub mod credential;
pub mod error;
pub mod mock;
#[derive(Default, Debug)]
struct EntryBuilder {
inner: Option<Box<CredentialBuilder>>,
}
static DEFAULT_BUILDER: std::sync::RwLock<EntryBuilder> =
std::sync::RwLock::new(EntryBuilder { inner: None });
pub fn set_default_credential_builder(new: Box<CredentialBuilder>) {
let mut guard = DEFAULT_BUILDER
.write()
.expect("Poisoned RwLock in keyring-rs: please report a bug!");
guard.inner = Some(new);
}
fn build_default_credential(target: Option<&str>, service: &str, user: &str) -> Result<Entry> {
lazy_static::lazy_static! {
static ref DEFAULT: Box<CredentialBuilder> = default::default_credential_builder();
}
let guard = DEFAULT_BUILDER
.read()
.expect("Poisoned RwLock in keyring-rs: please report a bug!");
let builder = match guard.inner.as_ref() {
Some(builder) => builder,
None => &DEFAULT,
};
let credential = builder.build(target, service, user)?;
Ok(Entry { inner: credential })
}
#[derive(Debug)]
pub struct Entry {
inner: Box<Credential>,
}
impl Entry {
pub fn new(service: &str, user: &str) -> Result<Entry> {
build_default_credential(None, service, user)
}
pub fn new_with_target(target: &str, service: &str, user: &str) -> Result<Entry> {
build_default_credential(Some(target), service, user)
}
pub fn new_with_credential(credential: Box<Credential>) -> Entry {
Entry { inner: credential }
}
pub fn set_password(&self, password: &str) -> Result<()> {
self.inner.set_password(password)
}
pub fn get_password(&self) -> Result<String> {
self.inner.get_password()
}
pub fn delete_password(&self) -> Result<()> {
self.inner.delete_password()
}
pub fn get_credential(&self) -> &dyn std::any::Any {
self.inner.as_any()
}
}
#[cfg(test)]
doc_comment::doctest!("../README.md");
#[cfg(test)]
#[allow(dead_code)]
mod tests {
use super::{credential::CredentialApi, Entry, Error, Result};
pub fn entry_from_constructor<F, T>(f: F, service: &str, user: &str) -> Entry
where
F: FnOnce(Option<&str>, &str, &str) -> Result<T>,
T: 'static + CredentialApi + Send + Sync,
{
match f(None, service, user) {
Ok(credential) => Entry::new_with_credential(Box::new(credential)),
Err(err) => {
panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
}
}
}
pub fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
entry
.set_password(in_pass)
.unwrap_or_else(|err| panic!("Can't set password for {case}: {err:?}"));
let out_pass = entry
.get_password()
.unwrap_or_else(|err| panic!("Can't get password for {case}: {err:?}"));
assert_eq!(
in_pass, out_pass,
"Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
);
entry
.delete_password()
.unwrap_or_else(|err| panic!("Can't delete password for {case}: {err:?}"));
let password = entry.get_password();
assert!(
matches!(password, Err(Error::NoEntry)),
"Read deleted password for {case}",
);
}
pub fn generate_random_string_of_len(len: usize) -> String {
use rand::{distributions::Alphanumeric, thread_rng, Rng};
thread_rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect()
}
pub fn generate_random_string() -> String {
generate_random_string_of_len(30)
}
pub fn test_empty_service_and_user<F>(f: F)
where
F: Fn(&str, &str) -> Entry,
{
let name = generate_random_string();
let in_pass = "doesn't matter";
test_round_trip("empty user", &f(&name, ""), in_pass);
test_round_trip("empty service", &f("", &name), in_pass);
test_round_trip("empty service & user", &f("", ""), in_pass);
}
pub fn test_missing_entry<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
assert!(
matches!(entry.get_password(), Err(Error::NoEntry)),
"Missing entry has password"
)
}
pub fn test_empty_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("empty password", &entry, "");
}
pub fn test_round_trip_ascii_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("ascii password", &entry, "test ascii password");
}
pub fn test_round_trip_non_ascii_password<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
}
pub fn test_update<F>(f: F)
where
F: FnOnce(&str, &str) -> Entry,
{
let name = generate_random_string();
let entry = f(&name, &name);
test_round_trip("initial ascii password", &entry, "test ascii password");
test_round_trip(
"updated non-ascii password",
&entry,
"このきれいな花は桜です",
);
}
}