Skip to main content

dcrypt_api/error/
registry.rs

1//! Error registry for compatibility with deferred error handling APIs.
2
3#[cfg(feature = "std")]
4use core::any::Any;
5#[cfg(not(feature = "std"))]
6use core::sync::atomic::{AtomicBool, Ordering};
7#[cfg(feature = "std")]
8use std::boxed::Box;
9#[cfg(feature = "std")]
10use std::sync::{Mutex, MutexGuard};
11
12/// Global error registry for recording the most recent deferred error.
13///
14/// The registry is retained for API compatibility. Prefer returning errors to
15/// the caller directly: a process-global "last error" can be overwritten by an
16/// unrelated operation at any time.
17pub static ERROR_REGISTRY: ErrorRegistry = ErrorRegistry::new();
18
19/// A synchronized, type-checked registry for a single error value.
20///
21/// Under `std`, stored errors are owned by a `Mutex` and retrieved using a
22/// checked [`core::any::Any`] downcast. A request for a different type returns
23/// `None`. Under `no_std`, where this crate has no synchronization primitive
24/// capable of owning an arbitrary allocation, the registry records only
25/// whether an error occurred.
26pub struct ErrorRegistry {
27    #[cfg(feature = "std")]
28    state: Mutex<RegistryState>,
29    #[cfg(not(feature = "std"))]
30    has_error: AtomicBool,
31}
32
33#[cfg(feature = "std")]
34struct RegistryState {
35    error: Option<Box<dyn Any + Send>>,
36    generation: u64,
37}
38
39impl ErrorRegistry {
40    /// Create a new, empty error registry.
41    pub const fn new() -> Self {
42        Self {
43            #[cfg(feature = "std")]
44            state: Mutex::new(RegistryState {
45                error: None,
46                generation: 0,
47            }),
48            #[cfg(not(feature = "std"))]
49            has_error: AtomicBool::new(false),
50        }
51    }
52
53    /// Store an error in the registry, replacing the previous value.
54    ///
55    /// Errors must be owned and safe to move between threads because the
56    /// registry is process-global. The replaced value is dropped after the
57    /// registry lock is released so user-defined destructors cannot deadlock
58    /// the registry by re-entering it.
59    pub fn store<E>(&self, error: E)
60    where
61        E: Send + 'static,
62    {
63        #[cfg(feature = "std")]
64        {
65            let old = {
66                let mut state = self.lock();
67                state.generation = state.generation.wrapping_add(1);
68                state.error.replace(Box::new(error))
69            };
70            drop(old);
71        }
72
73        #[cfg(not(feature = "std"))]
74        {
75            drop(error);
76            self.has_error.store(true, Ordering::Release);
77        }
78    }
79
80    /// Clear and drop the stored error, if any.
81    pub fn clear(&self) {
82        #[cfg(feature = "std")]
83        {
84            let old = {
85                let mut state = self.lock();
86                state.generation = state.generation.wrapping_add(1);
87                state.error.take()
88            };
89            drop(old);
90        }
91
92        #[cfg(not(feature = "std"))]
93        self.has_error.store(false, Ordering::Release);
94    }
95
96    /// Check whether an error is currently present.
97    pub fn has_error(&self) -> bool {
98        #[cfg(feature = "std")]
99        {
100            self.lock().error.is_some()
101        }
102
103        #[cfg(not(feature = "std"))]
104        {
105            self.has_error.load(Ordering::Acquire)
106        }
107    }
108
109    /// Clone the stored error if its concrete type is exactly `E`.
110    ///
111    /// A type mismatch returns `None`; callers can never reinterpret the
112    /// allocation as another type.
113    #[cfg(feature = "std")]
114    pub fn get_error<E>(&self) -> Option<E>
115    where
116        E: Clone + Send + 'static,
117    {
118        // Temporarily remove the allocation so user-defined `Clone` code runs
119        // without the registry mutex held. If another operation mutates the
120        // registry while cloning, that newer operation wins and the old value
121        // is dropped instead of being restored.
122        let (stored, generation) = {
123            let mut state = self.lock();
124            let stored = state.error.take()?;
125            (stored, state.generation)
126        };
127
128        let cloned = stored.downcast_ref::<E>().cloned();
129
130        let displaced = {
131            let mut state = self.lock();
132            if state.generation == generation && state.error.is_none() {
133                state.error = Some(stored);
134                None
135            } else {
136                Some(stored)
137            }
138        };
139        drop(displaced);
140
141        cloned
142    }
143
144    #[cfg(feature = "std")]
145    fn lock(&self) -> MutexGuard<'_, RegistryState> {
146        // A panic in a user-provided Clone implementation can poison the
147        // mutex. Poisoning does not make the owned value unsafe, so recover the
148        // guard and keep the registry usable.
149        self.state
150            .lock()
151            .unwrap_or_else(std::sync::PoisonError::into_inner)
152    }
153}
154
155impl Default for ErrorRegistry {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161impl Drop for ErrorRegistry {
162    fn drop(&mut self) {
163        #[cfg(feature = "std")]
164        {
165            let state = self
166                .state
167                .get_mut()
168                .unwrap_or_else(std::sync::PoisonError::into_inner);
169            drop(state.error.take());
170        }
171
172        #[cfg(not(feature = "std"))]
173        self.has_error.store(false, Ordering::Release);
174    }
175}
176
177#[cfg(all(test, feature = "std"))]
178mod tests {
179    use super::ErrorRegistry;
180    use std::sync::atomic::{AtomicUsize, Ordering};
181    use std::sync::{Arc, Barrier};
182    use std::thread;
183
184    #[test]
185    fn retrieves_only_the_stored_concrete_type() {
186        let registry = ErrorRegistry::new();
187        registry.store([0xA5_u8; 4096]);
188
189        assert_eq!(registry.get_error::<u8>(), None);
190        assert_eq!(registry.get_error::<String>(), None);
191        assert_eq!(registry.get_error::<[u8; 4096]>(), Some([0xA5; 4096]));
192    }
193
194    #[test]
195    fn replacing_and_clearing_drop_each_value_exactly_once() {
196        #[derive(Clone)]
197        struct DropTracker(Arc<AtomicUsize>);
198
199        impl Drop for DropTracker {
200            fn drop(&mut self) {
201                self.0.fetch_add(1, Ordering::SeqCst);
202            }
203        }
204
205        let drops = Arc::new(AtomicUsize::new(0));
206        let registry = ErrorRegistry::new();
207
208        registry.store(DropTracker(Arc::clone(&drops)));
209        registry.store(DropTracker(Arc::clone(&drops)));
210        assert_eq!(drops.load(Ordering::SeqCst), 1);
211
212        registry.clear();
213        assert_eq!(drops.load(Ordering::SeqCst), 2);
214        assert!(!registry.has_error());
215    }
216
217    #[test]
218    fn concurrent_store_get_and_clear_keep_values_owned() {
219        #[derive(Clone, Debug)]
220        struct Payload {
221            value: usize,
222            complement: usize,
223            padding: [usize; 16],
224        }
225
226        #[derive(Clone, Debug)]
227        struct SmallPayload {
228            value: usize,
229            complement: usize,
230        }
231
232        let registry = Arc::new(ErrorRegistry::new());
233        let barrier = Arc::new(Barrier::new(8));
234        let mut threads = Vec::new();
235        let iterations = if cfg!(miri) { 16 } else { 2_000 };
236
237        for worker in 0..8 {
238            let registry = Arc::clone(&registry);
239            let barrier = Arc::clone(&barrier);
240            threads.push(thread::spawn(move || {
241                barrier.wait();
242                for sequence in 0..iterations {
243                    let value = (worker << 24) | sequence;
244                    if sequence % 2 == 0 {
245                        registry.store(Payload {
246                            value,
247                            complement: !value,
248                            padding: [value; 16],
249                        });
250                    } else {
251                        registry.store(SmallPayload {
252                            value,
253                            complement: !value,
254                        });
255                    }
256
257                    if let Some(payload) = registry.get_error::<Payload>() {
258                        assert_eq!(payload.complement, !payload.value);
259                        assert!(payload.padding.iter().all(|item| *item == payload.value));
260                    }
261
262                    if let Some(payload) = registry.get_error::<SmallPayload>() {
263                        assert_eq!(payload.complement, !payload.value);
264                    }
265
266                    if sequence % 7 == 0 {
267                        registry.clear();
268                    }
269                }
270            }));
271        }
272
273        for thread in threads {
274            thread.join().unwrap();
275        }
276    }
277
278    #[test]
279    fn user_clone_can_reenter_registry_without_deadlock() {
280        struct ReentrantClone(Arc<ErrorRegistry>);
281
282        impl Clone for ReentrantClone {
283            fn clone(&self) -> Self {
284                // The value is temporarily out of the registry while Clone is
285                // invoked, so this reentrant call neither deadlocks nor sees a
286                // borrowed allocation that another thread could free.
287                assert!(!self.0.has_error());
288                Self(Arc::clone(&self.0))
289            }
290        }
291
292        let registry = Arc::new(ErrorRegistry::new());
293        registry.store(ReentrantClone(Arc::clone(&registry)));
294
295        assert!(registry.get_error::<ReentrantClone>().is_some());
296        assert!(registry.has_error());
297        // Break the deliberate Arc cycle created for this reentrancy test so
298        // leak-checking interpreters can verify the registry itself cleanly.
299        registry.clear();
300        assert!(!registry.has_error());
301    }
302}
303
304#[cfg(all(test, not(feature = "std")))]
305mod no_std_tests {
306    use super::ErrorRegistry;
307    use core::sync::atomic::{AtomicUsize, Ordering};
308
309    static DROPS: AtomicUsize = AtomicUsize::new(0);
310
311    struct DropTracker;
312
313    impl Drop for DropTracker {
314        fn drop(&mut self) {
315            DROPS.fetch_add(1, Ordering::SeqCst);
316        }
317    }
318
319    #[test]
320    fn presence_only_registry_drops_values_and_tracks_state() {
321        DROPS.store(0, Ordering::SeqCst);
322        let registry = ErrorRegistry::new();
323
324        registry.store(DropTracker);
325        assert_eq!(DROPS.load(Ordering::SeqCst), 1);
326        assert!(registry.has_error());
327
328        registry.clear();
329        assert!(!registry.has_error());
330    }
331}