Skip to main content

azul_layout/managers/
keyring.rs

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
20use 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.
25pub use azul_core::keyring::{KeyringRequest, KeyringResult};
26
27use azul_core::dom::DomNodeId;
28use azul_core::events::{
29    EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
30};
31use azul_core::task::Instant;
32
33/// Cross-platform keyring state. One per `App` — the OS keyring is a
34/// per-process (per-app-identity) store, not per-window.
35#[derive(Debug, Clone, PartialEq, Eq, Default)]
36pub struct KeyringManager {
37    /// Outcome of the most recent keyring op, or `None` until the first
38    /// completes. Read by callbacks via `CallbackInfo::get_keyring_result()`.
39    pub last_result: Option<KeyringResult>,
40    /// Ops dispatched to the native backend whose outcome has not been
41    /// folded back yet (MWA-A1b arming signal for the capability pump).
42    pub in_flight: u32,
43    /// `true` when an op outcome was folded since the last event pass (set
44    /// on EVERY completion — a repeated identical outcome still answers a
45    /// fresh op). Read by the `EventProvider` impl
46    /// (`EventType::KeyringResult`), cleared by
47    /// [`clear_pending_event`](Self::clear_pending_event).
48    pub pending_event: bool,
49}
50
51impl KeyringManager {
52    #[must_use] pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Most recent keyring outcome, or `None` until the first op resolves.
57    #[must_use] pub const fn last_result(&self) -> Option<&KeyringResult> {
58        self.last_result.as_ref()
59    }
60
61    /// Apply the outcome the backend delivered. Returns `true` if it
62    /// differs from the previous one (so the window can be marked dirty to
63    /// re-render the revealed / stored state).
64    pub fn set_last_result(&mut self, result: KeyringResult) -> bool {
65        let changed = self.last_result.as_ref() != Some(&result);
66        self.last_result = Some(result);
67        // MWA-A1b: every completion fires an event and retires one
68        // in-flight op.
69        self.pending_event = true;
70        self.in_flight = self.in_flight.saturating_sub(1);
71        changed
72    }
73
74    /// The pump dispatched `n` ops to the native backend; keep the timer
75    /// armed until their outcomes fold back (MWA-A1b).
76    pub const fn mark_requests_dispatched(&mut self, n: u32) {
77        self.in_flight = self.in_flight.saturating_add(n);
78    }
79
80    /// Clear the pending-event flag. The dll calls this after the event
81    /// pass has collected the `KeyringResult` event.
82    pub const fn clear_pending_event(&mut self) {
83        self.pending_event = false;
84    }
85
86    /// `true` while a dispatched op's outcome is still outstanding
87    /// (MWA-A1b arming signal).
88    #[must_use] pub const fn has_pending_async(&self) -> bool {
89        self.in_flight > 0
90    }
91}
92
93impl EventProvider for KeyringManager {
94    /// Yield a window-level `KeyringResult` event when an op outcome was
95    /// folded since the last pass (target = root; read the outcome via
96    /// `CallbackInfo::get_keyring_result` inside the callback).
97    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
98        if self.pending_event {
99            alloc::vec![SyntheticEvent::new(
100                EventType::KeyringResult,
101                CoreEventSource::User,
102                DomNodeId::ROOT,
103                timestamp,
104                EventData::None,
105            )]
106        } else {
107            Vec::new()
108        }
109    }
110}
111
112// ────────── Request channel (callback → platform backend) ─────────────
113
114static PENDING_REQUESTS: std::sync::Mutex<Vec<KeyringRequest>> =
115    std::sync::Mutex::new(Vec::new());
116
117/// Queue a keyring op from a callback. Drained by the dll layout pass and
118/// dispatched to the native keyring. Thread-safe; poison-recovering.
119pub fn push_keyring_request(request: KeyringRequest) {
120    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
121    q.push(request);
122}
123
124/// Drain every queued keyring op, in arrival order. Called once per
125/// layout pass; the dll dispatches each to the platform backend.
126pub fn drain_keyring_requests() -> Vec<KeyringRequest> {
127    let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
128    core::mem::take(&mut *q)
129}
130
131/// MWA-C-biometric/keyring: see `biometric::has_queued_requests` — pump
132/// arming must count parked-but-undispatched requests.
133pub fn has_queued_requests() -> bool {
134    PENDING_REQUESTS
135        .lock()
136        .map_or_else(|e| !e.into_inner().is_empty(), |q| !q.is_empty())
137}
138
139// ────────── Result channel (platform backend → manager) ───────────────
140
141static PENDING_RESULTS: std::sync::Mutex<Vec<KeyringResult>> =
142    std::sync::Mutex::new(Vec::new());
143
144/// Park a keyring result delivered by a platform backend (in the dll).
145/// Thread-safe; poison-recovering (a biometry-bound `Get` resolves from
146/// the OS prompt's reply on an arbitrary thread).
147pub fn push_keyring_result(result: KeyringResult) {
148    let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
149    q.push(result);
150}
151
152/// Drain every parked keyring result, in arrival order. Called once per
153/// layout pass; the caller applies them via [`KeyringManager::set_last_result`].
154pub fn drain_keyring_results() -> Vec<KeyringResult> {
155    let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
156    core::mem::take(&mut *q)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use azul_css::AzString;
163
164    #[test]
165    fn manager_defaults_to_no_result() {
166        let mgr = KeyringManager::new();
167        assert_eq!(mgr.last_result(), None);
168    }
169
170    #[test]
171    fn set_last_result_returns_change_flag() {
172        let mut mgr = KeyringManager::new();
173        assert!(mgr.set_last_result(KeyringResult::Stored));
174        assert_eq!(mgr.last_result(), Some(&KeyringResult::Stored));
175        // Re-applying the same outcome is not a change.
176        assert!(!mgr.set_last_result(KeyringResult::Stored));
177        // A new outcome is a change.
178        assert!(mgr.set_last_result(KeyringResult::Deleted));
179    }
180
181    #[test]
182    fn result_helpers() {
183        let secret = KeyringResult::Retrieved(AzString::from_const_str("hunter2"));
184        assert_eq!(secret.secret().map(AzString::as_str), Some("hunter2"));
185        assert!(secret.is_ok());
186        assert!(KeyringResult::Stored.is_ok());
187        assert!(KeyringResult::Deleted.is_ok());
188        for r in [
189            KeyringResult::NotFound,
190            KeyringResult::Denied,
191            KeyringResult::Unavailable,
192            KeyringResult::Error,
193        ] {
194            assert!(!r.is_ok(), "{r:?} must not be ok");
195            assert_eq!(r.secret(), None);
196        }
197    }
198
199    #[test]
200    fn requests_round_trip_through_channel() {
201        drop(drain_keyring_requests());
202
203        push_keyring_request(KeyringRequest::Store {
204            key: AzString::from_const_str("token"),
205            secret: AzString::from_const_str("abc"),
206            require_biometry: true,
207        });
208        push_keyring_request(KeyringRequest::Get {
209            key: AzString::from_const_str("token"),
210        });
211        let drained = drain_keyring_requests();
212        assert_eq!(drained.len(), 2, "both queued requests drain in order");
213        assert!(matches!(drained[0], KeyringRequest::Store { .. }));
214        assert!(matches!(drained[1], KeyringRequest::Get { .. }));
215        assert!(drain_keyring_requests().is_empty());
216    }
217
218    #[test]
219    fn results_round_trip_through_manager() {
220        drop(drain_keyring_results());
221
222        push_keyring_result(KeyringResult::NotFound);
223        push_keyring_result(KeyringResult::Retrieved(AzString::from_const_str("s"))); // last wins
224        let drained = drain_keyring_results();
225        assert_eq!(drained.len(), 2);
226
227        let mut mgr = KeyringManager::new();
228        for r in drained {
229            mgr.set_last_result(r);
230        }
231        assert_eq!(
232            mgr.last_result().and_then(|r| r.secret()).map(AzString::as_str),
233            Some("s"),
234            "the last applied result wins"
235        );
236        assert!(drain_keyring_results().is_empty());
237    }
238}
239
240#[cfg(test)]
241mod pump_provider_tests {
242    use super::*;
243    use azul_core::task::{Instant, SystemTick};
244
245    fn ts() -> Instant {
246        Instant::Tick(SystemTick::new(0))
247    }
248
249    #[test]
250    fn in_flight_and_events_track_op_lifecycle() {
251        let mut mgr = KeyringManager::new();
252        assert!(!mgr.has_pending_async());
253        mgr.mark_requests_dispatched(1);
254        assert!(mgr.has_pending_async());
255        assert!(mgr.get_pending_events(ts()).is_empty(), "no outcome yet");
256
257        mgr.set_last_result(KeyringResult::Stored);
258        assert!(!mgr.has_pending_async());
259        let events = mgr.get_pending_events(ts());
260        assert_eq!(events.len(), 1);
261        assert_eq!(events[0].event_type, EventType::KeyringResult);
262        mgr.clear_pending_event();
263
264        // repeated identical outcome still fires — it answers a fresh op
265        mgr.mark_requests_dispatched(1);
266        mgr.set_last_result(KeyringResult::Stored);
267        assert_eq!(mgr.get_pending_events(ts()).len(), 1);
268    }
269}