Skip to main content

git_lfs_creds/
memory.rs

1//! Process-local credential cache.
2//!
3//! Avoids re-shelling-out to `git credential` for every request once we've
4//! resolved a working set of credentials for a given (protocol, host, path).
5//!
6//! Scope is a single CLI invocation — for a long-running daemon you'd want
7//! a TTL on top of this. Not relevant for our short-lived CLI subcommands.
8
9use std::collections::HashMap;
10use std::sync::Mutex;
11
12use crate::helper::{Credentials, Helper, HelperError};
13use crate::query::Query;
14
15/// Process-local credential cache, keyed on the full [`Query`] tuple.
16///
17/// Populated by [`Helper::approve`] and consulted on [`Helper::fill`];
18/// [`Helper::reject`] drops the corresponding entry. Lives for one
19/// CLI invocation; a long-running daemon would want a TTL layered on
20/// top.
21#[derive(Debug, Default)]
22pub struct CachingHelper {
23    cache: Mutex<HashMap<Query, Credentials>>,
24}
25
26impl CachingHelper {
27    /// Create an empty cache.
28    pub fn new() -> Self {
29        Self::default()
30    }
31}
32
33impl Helper for CachingHelper {
34    fn fill(&self, query: &Query) -> Result<Option<Credentials>, HelperError> {
35        Ok(self.cache.lock().unwrap().get(query).cloned())
36    }
37
38    /// Cache the working credentials so the next request skips the helper
39    /// chain entirely.
40    fn approve(&self, query: &Query, creds: &Credentials) -> Result<(), HelperError> {
41        self.cache
42            .lock()
43            .unwrap()
44            .insert(query.clone(), creds.clone());
45        Ok(())
46    }
47
48    /// Drop the cached entry — whatever's in there clearly didn't work.
49    fn reject(&self, query: &Query, _creds: &Credentials) -> Result<(), HelperError> {
50        self.cache.lock().unwrap().remove(query);
51        Ok(())
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    fn q() -> Query {
60        Query {
61            protocol: "https".into(),
62            host: "git.example.com".into(),
63            path: String::new(),
64        }
65    }
66
67    #[test]
68    fn fill_misses_until_approve() {
69        let h = CachingHelper::new();
70        assert!(h.fill(&q()).unwrap().is_none());
71        let c = Credentials::new("alice", "hunter2");
72        h.approve(&q(), &c).unwrap();
73        assert_eq!(h.fill(&q()).unwrap(), Some(c));
74    }
75
76    #[test]
77    fn reject_evicts() {
78        let h = CachingHelper::new();
79        let c = Credentials::new("alice", "hunter2");
80        h.approve(&q(), &c).unwrap();
81        h.reject(&q(), &c).unwrap();
82        assert!(h.fill(&q()).unwrap().is_none());
83    }
84
85    #[test]
86    fn fill_keys_on_full_query_tuple() {
87        let h = CachingHelper::new();
88        let c = Credentials::new("alice", "hunter2");
89        h.approve(&q(), &c).unwrap();
90        let other = Query {
91            protocol: "https".into(),
92            host: "other.example.com".into(),
93            path: String::new(),
94        };
95        assert!(h.fill(&other).unwrap().is_none());
96    }
97}