1
//! Keyring manager — cross-platform state for the system-keyring surface
2
//! (SUPER_PLAN_2 §4 P4.2).
3
//!
4
//! Request-driven, mirroring [`crate::managers::biometric`]:
5
//!
6
//! - A **callback** calls `CallbackInfo::keyring_store/get/delete(...)`,
7
//!   which parks a [`KeyringRequest`] in the request channel.
8
//! - The dll **layout pass** drains it and dispatches to the platform
9
//!   backend (`dll::desktop::extra::keyring`) — Keychain / KeyStore /
10
//!   libsecret / CredentialLocker. A biometry-bound `Get` shows the OS
11
//!   prompt; the outcome is parked in the result channel.
12
//! - The layout pass folds the latest result into the manager via
13
//!   [`KeyringManager::set_last_result`]; callbacks read it with
14
//!   `CallbackInfo::get_keyring_result()`.
15
//!
16
//! No platform deps (SUPER_PLAN_2 §0.5); the channels are the same
17
//! poison-recovering `Mutex<Vec<_>>` pattern as the geolocation /
18
//! biometric managers.
19

            
20
use alloc::vec::Vec;
21

            
22
// `KeyringRequest` / `KeyringResult` live in `azul-core` so they cross the
23
// FFI without a cyclic dep on `azul-layout`. Re-exported for the existing
24
// `azul_layout::managers::keyring::*` import paths.
25
pub use azul_core::keyring::{KeyringRequest, KeyringResult};
26

            
27
/// Cross-platform keyring state. One per `App` — the OS keyring is a
28
/// per-process (per-app-identity) store, not per-window.
29
#[derive(Debug, Clone, PartialEq, Default)]
30
pub struct KeyringManager {
31
    /// Outcome of the most recent keyring op, or `None` until the first
32
    /// completes. Read by callbacks via `CallbackInfo::get_keyring_result()`.
33
    pub last_result: Option<KeyringResult>,
34
}
35

            
36
impl KeyringManager {
37
2324
    pub fn new() -> Self {
38
2324
        Self::default()
39
2324
    }
40

            
41
    /// Most recent keyring outcome, or `None` until the first op resolves.
42
3
    pub fn last_result(&self) -> Option<&KeyringResult> {
43
3
        self.last_result.as_ref()
44
3
    }
45

            
46
    /// Apply the outcome the backend delivered. Returns `true` if it
47
    /// differs from the previous one (so the window can be marked dirty to
48
    /// re-render the revealed / stored state).
49
5
    pub fn set_last_result(&mut self, result: KeyringResult) -> bool {
50
5
        let changed = self.last_result.as_ref() != Some(&result);
51
5
        self.last_result = Some(result);
52
5
        changed
53
5
    }
54
}
55

            
56
// ────────── Request channel (callback → platform backend) ─────────────
57

            
58
static PENDING_REQUESTS: std::sync::Mutex<Vec<KeyringRequest>> =
59
    std::sync::Mutex::new(Vec::new());
60

            
61
/// Queue a keyring op from a callback. Drained by the dll layout pass and
62
/// dispatched to the native keyring. Thread-safe; poison-recovering.
63
2
pub fn push_keyring_request(request: KeyringRequest) {
64
2
    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(|e| e.into_inner());
65
2
    q.push(request);
66
2
}
67

            
68
/// Drain every queued keyring op, in arrival order. Called once per
69
/// layout pass; the dll dispatches each to the platform backend.
70
3
pub fn drain_keyring_requests() -> Vec<KeyringRequest> {
71
3
    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(|e| e.into_inner());
72
3
    core::mem::take(&mut *q)
73
3
}
74

            
75
// ────────── Result channel (platform backend → manager) ───────────────
76

            
77
static PENDING_RESULTS: std::sync::Mutex<Vec<KeyringResult>> =
78
    std::sync::Mutex::new(Vec::new());
79

            
80
/// Park a keyring result delivered by a platform backend (in the dll).
81
/// Thread-safe; poison-recovering (a biometry-bound `Get` resolves from
82
/// the OS prompt's reply on an arbitrary thread).
83
2
pub fn push_keyring_result(result: KeyringResult) {
84
2
    let mut q = PENDING_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
85
2
    q.push(result);
86
2
}
87

            
88
/// Drain every parked keyring result, in arrival order. Called once per
89
/// layout pass; the caller applies them via [`KeyringManager::set_last_result`].
90
3
pub fn drain_keyring_results() -> Vec<KeyringResult> {
91
3
    let mut q = PENDING_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
92
3
    core::mem::take(&mut *q)
93
3
}
94

            
95
#[cfg(test)]
96
mod tests {
97
    use super::*;
98
    use azul_css::AzString;
99

            
100
    #[test]
101
1
    fn manager_defaults_to_no_result() {
102
1
        let mgr = KeyringManager::new();
103
1
        assert_eq!(mgr.last_result(), None);
104
1
    }
105

            
106
    #[test]
107
1
    fn set_last_result_returns_change_flag() {
108
1
        let mut mgr = KeyringManager::new();
109
1
        assert!(mgr.set_last_result(KeyringResult::Stored));
110
1
        assert_eq!(mgr.last_result(), Some(&KeyringResult::Stored));
111
        // Re-applying the same outcome is not a change.
112
1
        assert!(!mgr.set_last_result(KeyringResult::Stored));
113
        // A new outcome is a change.
114
1
        assert!(mgr.set_last_result(KeyringResult::Deleted));
115
1
    }
116

            
117
    #[test]
118
1
    fn result_helpers() {
119
1
        let secret = KeyringResult::Retrieved(AzString::from_const_str("hunter2"));
120
1
        assert_eq!(secret.secret().map(|s| s.as_str()), Some("hunter2"));
121
1
        assert!(secret.is_ok());
122
1
        assert!(KeyringResult::Stored.is_ok());
123
1
        assert!(KeyringResult::Deleted.is_ok());
124
4
        for r in [
125
1
            KeyringResult::NotFound,
126
1
            KeyringResult::Denied,
127
1
            KeyringResult::Unavailable,
128
1
            KeyringResult::Error,
129
        ] {
130
4
            assert!(!r.is_ok(), "{:?} must not be ok", r);
131
4
            assert_eq!(r.secret(), None);
132
        }
133
1
    }
134

            
135
    #[test]
136
1
    fn requests_round_trip_through_channel() {
137
1
        let _ = drain_keyring_requests();
138

            
139
1
        push_keyring_request(KeyringRequest::Store {
140
1
            key: AzString::from_const_str("token"),
141
1
            secret: AzString::from_const_str("abc"),
142
1
            require_biometry: true,
143
1
        });
144
1
        push_keyring_request(KeyringRequest::Get {
145
1
            key: AzString::from_const_str("token"),
146
1
        });
147
1
        let drained = drain_keyring_requests();
148
1
        assert_eq!(drained.len(), 2, "both queued requests drain in order");
149
1
        assert!(matches!(drained[0], KeyringRequest::Store { .. }));
150
1
        assert!(matches!(drained[1], KeyringRequest::Get { .. }));
151
1
        assert!(drain_keyring_requests().is_empty());
152
1
    }
153

            
154
    #[test]
155
1
    fn results_round_trip_through_manager() {
156
1
        let _ = drain_keyring_results();
157

            
158
1
        push_keyring_result(KeyringResult::NotFound);
159
1
        push_keyring_result(KeyringResult::Retrieved(AzString::from_const_str("s"))); // last wins
160
1
        let drained = drain_keyring_results();
161
1
        assert_eq!(drained.len(), 2);
162

            
163
1
        let mut mgr = KeyringManager::new();
164
3
        for r in drained {
165
2
            mgr.set_last_result(r);
166
2
        }
167
1
        assert_eq!(
168
1
            mgr.last_result().and_then(|r| r.secret()).map(|s| s.as_str()),
169
            Some("s"),
170
            "the last applied result wins"
171
        );
172
1
        assert!(drain_keyring_results().is_empty());
173
1
    }
174
}