1use std::cell::RefCell;
41use std::collections::HashMap;
42use std::sync::{Arc, Mutex};
43use std::time::{Duration, SystemTime, UNIX_EPOCH};
44
45use crate::api::{CredentialApi, CredentialStoreApi};
46use crate::{Credential, CredentialPersistence, Entry, Error, Result};
47
48#[derive(Debug)]
53pub struct Cred {
54 pub specifiers: (String, String),
55 pub inner: Mutex<RefCell<CredData>>,
56}
57
58#[derive(Debug, Default)]
66pub struct CredData {
67 pub secret: Option<Vec<u8>>,
68 pub error: Option<Error>,
69}
70
71impl CredentialApi for Cred {
72 fn set_secret(&self, secret: &[u8]) -> Result<()> {
78 let mut inner = self
79 .inner
80 .lock()
81 .expect("Can't access mock data for set_secret: please report a bug!");
82 let data = inner.get_mut();
83 let err = data.error.take();
84 match err {
85 None => {
86 data.secret = Some(secret.to_vec());
87 Ok(())
88 }
89 Some(err) => Err(err),
90 }
91 }
92
93 fn get_secret(&self) -> Result<Vec<u8>> {
99 let mut inner = self
100 .inner
101 .lock()
102 .expect("Can't access mock data for get: please report a bug!");
103 let data = inner.get_mut();
104 let err = data.error.take();
105 match err {
106 None => match &data.secret {
107 None => Err(Error::NoEntry),
108 Some(val) => Ok(val.clone()),
109 },
110 Some(err) => Err(err),
111 }
112 }
113
114 fn delete_credential(&self) -> Result<()> {
119 let mut inner = self
120 .inner
121 .lock()
122 .expect("Can't access mock data for delete: please report a bug!");
123 let data = inner.get_mut();
124 let err = data.error.take();
125 match err {
126 None => match data.secret {
127 Some(_) => {
128 data.secret = None;
129 Ok(())
130 }
131 None => Err(Error::NoEntry),
132 },
133 Some(err) => Err(err),
134 }
135 }
136
137 fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
142 let mut inner = self
143 .inner
144 .lock()
145 .expect("Can't access mock data for get_credential: please report a bug!");
146 let data = inner.get_mut();
147 let err = data.error.take();
148 match err {
149 None => match data.secret {
150 Some(_) => Ok(None),
151 None => Err(Error::NoEntry),
152 },
153 Some(err) => Err(err),
154 }
155 }
156
157 fn get_specifiers(&self) -> Option<(String, String)> {
159 Some(self.specifiers.clone())
160 }
161
162 fn as_any(&self) -> &dyn std::any::Any {
166 self
167 }
168
169 fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 std::fmt::Debug::fmt(self, f)
172 }
173}
174
175impl Cred {
176 pub fn set_error(&self, err: Error) {
182 let mut inner = self
183 .inner
184 .lock()
185 .expect("Can't access mock data for set_error: please report a bug!");
186 let data = inner.get_mut();
187 data.error = Some(err);
188 }
189}
190
191#[derive(Debug)]
198pub struct Store {
199 pub id: String,
200 pub inner: Mutex<RefCell<Vec<Arc<Cred>>>>,
201}
202
203impl Store {
204 pub fn new() -> Result<Arc<Self>> {
205 Ok(Arc::new(Store {
206 id: format!(
207 "Crate version {}, Instantiated at {}",
208 env!("CARGO_PKG_VERSION"),
209 SystemTime::now()
210 .duration_since(UNIX_EPOCH)
211 .unwrap_or_else(|_| Duration::new(0, 0))
212 .as_secs_f64()
213 ),
214 inner: Mutex::new(RefCell::new(Vec::new())),
215 }))
216 }
217}
218
219impl CredentialStoreApi for Store {
220 fn vendor(&self) -> String {
221 String::from("Mock in-memory store, https://crates.io/crates/keyring-core")
222 }
223
224 fn id(&self) -> String {
225 self.id.clone()
226 }
227
228 fn build(
233 &self,
234 service: &str,
235 user: &str,
236 mods: Option<&HashMap<&str, &str>>,
237 ) -> Result<Entry> {
238 if mods.is_some_and(|m| !m.is_empty()) {
239 return Err(Error::NotSupportedByStore(
240 "The mock store doesn't allow modifiers".to_string(),
241 ));
242 }
243 let mut inner = self
244 .inner
245 .lock()
246 .expect("Can't access mock store data: please report a bug!");
247 let creds = inner.get_mut();
248 for cred in creds.iter() {
249 if service == cred.specifiers.0 && user == cred.specifiers.1 {
250 return Ok(Entry {
251 inner: cred.clone(),
252 });
253 }
254 }
255 let cred = Arc::new(Cred {
256 specifiers: (service.to_string(), user.to_string()),
257 inner: Mutex::new(RefCell::new(Default::default())),
258 });
259 creds.push(cred.clone());
260 Ok(Entry { inner: cred })
261 }
262
263 fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
268 let mut result: Vec<Entry> = Vec::new();
269 let svc = spec.get("service").unwrap_or(&"");
270 let usr = spec.get("user").unwrap_or(&"");
271 let mut inner = self
272 .inner
273 .lock()
274 .expect("Can't access mock store data: please report a bug!");
275 let creds = inner.get_mut();
276 for cred in creds.iter() {
277 if !cred.specifiers.0.as_str().contains(svc) {
278 continue;
279 }
280 if !cred.specifiers.1.as_str().contains(usr) {
281 continue;
282 }
283 result.push(Entry {
284 inner: cred.clone(),
285 });
286 }
287 Ok(result)
288 }
289
290 fn as_any(&self) -> &dyn std::any::Any {
292 self
293 }
294
295 fn persistence(&self) -> CredentialPersistence {
297 CredentialPersistence::ProcessOnly
298 }
299
300 fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 std::fmt::Debug::fmt(self, f)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use std::sync::{Arc, Once};
310
311 use super::{Cred, HashMap, Store};
312 use crate::{CredentialPersistence, CredentialStore, Entry, Error, get_default_store};
313
314 static SET_STORE: Once = Once::new();
315
316 fn usually_goes_in_main() {
317 let _ = env_logger::builder().is_test(true).try_init();
318 crate::set_default_store(Store::new().unwrap());
319 }
320
321 #[test]
322 fn test_store_methods() {
323 SET_STORE.call_once(usually_goes_in_main);
324 let store = get_default_store().unwrap();
325 let vendor1 = store.vendor();
326 let id1 = store.id();
327 let vendor2 = store.vendor();
328 let id2 = store.id();
329 assert_eq!(vendor1, vendor2);
330 assert_eq!(id1, id2);
331 let store2: Arc<CredentialStore> = Store::new().unwrap();
332 let vendor3 = store2.vendor();
333 let id3 = store2.id();
334 assert_eq!(vendor1, vendor3);
335 assert_ne!(id1, id3);
336 }
337
338 fn entry_new(service: &str, user: &str) -> Entry {
339 SET_STORE.call_once(usually_goes_in_main);
340 Entry::new(service, user).unwrap_or_else(|err| {
341 panic!("Couldn't create entry (service: {service}, user: {user}): {err:?}")
342 })
343 }
344
345 fn generate_random_string() -> String {
346 use fastrand;
347 use std::iter::repeat_with;
348 repeat_with(fastrand::alphanumeric).take(30).collect()
349 }
350
351 fn generate_random_bytes() -> Vec<u8> {
352 use fastrand;
353 use std::iter::repeat_with;
354 repeat_with(|| fastrand::u8(..)).take(24).collect()
355 }
356
357 fn test_round_trip_no_delete(case: &str, entry: &Entry, in_pass: &str) {
359 entry
360 .set_password(in_pass)
361 .unwrap_or_else(|err| panic!("Can't set password: {case}: {err:?}"));
362 let out_pass = entry
363 .get_password()
364 .unwrap_or_else(|err| panic!("Can't get password: {case}: {err:?}"));
365 assert_eq!(
366 in_pass, out_pass,
367 "Passwords don't match for {case}: set='{in_pass}', get='{out_pass}'",
368 )
369 }
370
371 fn test_round_trip(case: &str, entry: &Entry, in_pass: &str) {
373 test_round_trip_no_delete(case, entry, in_pass);
374 entry
375 .delete_credential()
376 .unwrap_or_else(|err| panic!("Can't delete password: {case}: {err:?}"));
377 let password = entry.get_password();
378 assert!(matches!(password, Err(Error::NoEntry)));
379 }
380
381 pub fn test_round_trip_secret(case: &str, entry: &Entry, in_secret: &[u8]) {
383 entry
384 .set_secret(in_secret)
385 .unwrap_or_else(|err| panic!("Can't set secret for {case}: {err:?}"));
386 let out_secret = entry
387 .get_secret()
388 .unwrap_or_else(|err| panic!("Can't get secret for {case}: {err:?}"));
389 assert_eq!(
390 in_secret, &out_secret,
391 "Secrets don't match for {case}: set='{in_secret:?}', get='{out_secret:?}'",
392 );
393 entry
394 .delete_credential()
395 .unwrap_or_else(|err| panic!("Can't delete credential for {case}: {err:?}"));
396 let secret = entry.get_secret();
397 assert!(matches!(secret, Err(Error::NoEntry)));
398 }
399
400 #[test]
401 fn test_empty_service_and_user() {
402 let name = generate_random_string();
403 let in_pass = "value doesn't matter";
404 test_round_trip("empty user", &entry_new(&name, ""), in_pass);
405 test_round_trip("empty service", &entry_new("", &name), in_pass);
406 test_round_trip("empty service and user", &entry_new("", ""), in_pass);
407 }
408
409 #[test]
410 fn test_empty_password() {
411 let name = generate_random_string();
412 let in_pass = "";
413 test_round_trip("empty password", &entry_new(&name, &name), in_pass);
414 }
415
416 #[test]
417 fn test_missing_entry() {
418 let name = generate_random_string();
419 let entry = entry_new(&name, &name);
420 assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
421 }
422
423 #[test]
424 fn test_round_trip_ascii_password() {
425 let name = generate_random_string();
426 let entry = entry_new(&name, &name);
427 test_round_trip("ascii password", &entry, "test ascii password");
428 }
429
430 #[test]
431 fn test_round_trip_non_ascii_password() {
432 let name = generate_random_string();
433 let entry = entry_new(&name, &name);
434 test_round_trip("non-ascii password", &entry, "このきれいな花は桜です");
435 }
436
437 #[test]
438 fn test_entries_with_same_and_different_specifiers() {
439 let name1 = generate_random_string();
440 let name2 = generate_random_string();
441 let entry1 = entry_new(&name1, &name2);
442 let entry2 = entry_new(&name1, &name2);
443 let entry3 = entry_new(&name2, &name1);
444 entry1.set_password("test password").unwrap();
445 let pw2 = entry2.get_password().unwrap();
446 assert_eq!(pw2, "test password");
447 _ = entry3.get_password().unwrap_err();
448 entry1.delete_credential().unwrap();
449 _ = entry2.get_password().unwrap_err();
450 entry3.delete_credential().unwrap_err();
451 }
452
453 #[test]
454 fn test_get_credential_and_specifiers() {
455 let name = generate_random_string();
456 let entry1 = entry_new(&name, &name);
457 assert!(matches!(entry1.get_credential(), Err(Error::NoEntry)));
458 entry1.set_password("password for entry1").unwrap();
459 let wrapper = entry1.get_credential().unwrap();
460 let (service, user) = wrapper.get_specifiers().unwrap();
461 assert_eq!(service, name);
462 assert_eq!(user, name);
463 wrapper.delete_credential().unwrap();
464 entry1.delete_credential().unwrap_err();
465 wrapper.delete_credential().unwrap_err();
466 }
467
468 #[test]
469 fn test_round_trip_random_secret() {
470 let name = generate_random_string();
471 let entry = entry_new(&name, &name);
472 let secret = generate_random_bytes();
473 test_round_trip_secret("non-ascii password", &entry, secret.as_slice());
474 }
475
476 #[test]
477 fn test_update() {
478 let name = generate_random_string();
479 let entry = entry_new(&name, &name);
480 test_round_trip_no_delete("initial ascii password", &entry, "test ascii password");
481 test_round_trip(
482 "updated non-ascii password",
483 &entry,
484 "このきれいな花は桜です",
485 );
486 }
487
488 #[test]
489 fn test_set_error() {
490 let name = generate_random_string();
491 let entry = entry_new(&name, &name);
492 let password = "test ascii password";
493 let mock: &Cred = entry.inner.as_any().downcast_ref().unwrap();
494 mock.set_error(Error::Invalid(
495 "mock error".to_string(),
496 "is an error".to_string(),
497 ));
498 assert!(matches!(
499 entry.set_password(password),
500 Err(Error::Invalid(_, _))
501 ));
502 entry.set_password(password).unwrap();
503 mock.set_error(Error::NoEntry);
504 assert!(matches!(entry.get_password(), Err(Error::NoEntry)));
505 let stored_password = entry.get_password().unwrap();
506 assert_eq!(stored_password, password);
507 mock.set_error(Error::TooLong("mock".to_string(), 3));
508 assert!(matches!(
509 entry.delete_credential(),
510 Err(Error::TooLong(_, 3))
511 ));
512 entry.delete_credential().unwrap();
513 assert!(matches!(entry.get_password(), Err(Error::NoEntry)))
514 }
515
516 #[test]
517 fn test_search() {
518 let store: Arc<CredentialStore> = Store::new().unwrap();
519 let all = store.search(&HashMap::from([])).unwrap();
520 assert!(all.is_empty());
521 let all = store
522 .search(&HashMap::from([("service", ""), ("user", "")]))
523 .unwrap();
524 assert!(all.is_empty());
525 let e1 = store.build("foo", "bar", None).unwrap();
526 e1.set_password("e1").unwrap();
527 let all = store.search(&HashMap::from([])).unwrap();
528 assert_eq!(all.len(), 1);
529 let all = store
530 .search(&HashMap::from([("service", ""), ("user", "")]))
531 .unwrap();
532 assert_eq!(all.len(), 1);
533 let e2 = store.build("foo", "bam", None).unwrap();
534 e2.set_password("e2").unwrap();
535 let one = store.search(&HashMap::from([("user", "m")])).unwrap();
536 assert_eq!(one.len(), 1);
537 let one = store
538 .search(&HashMap::from([("service", "foo"), ("user", "bar")]))
539 .unwrap();
540 assert_eq!(one.len(), 1);
541 let two = store.search(&HashMap::from([("service", "foo")])).unwrap();
542 assert_eq!(two.len(), 2);
543 let all = store.search(&HashMap::from([("foo", "bar")])).unwrap();
544 assert_eq!(all.len(), 2);
545 }
546
547 #[test]
548 fn test_persistence() {
549 let store: Arc<CredentialStore> = Store::new().unwrap();
550 assert!(matches!(
551 store.persistence(),
552 CredentialPersistence::ProcessOnly
553 ))
554 }
555}