1use azul_css::AzString;
20
21#[repr(C, u8)]
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum KeyringRequest {
31 Store {
36 key: AzString,
37 secret: AzString,
38 require_biometry: bool,
39 },
40 Get { key: AzString },
43 Delete { key: AzString },
45}
46#[allow(variant_size_differences)] #[repr(C, u8)]
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum KeyringResult {
52 Stored,
54 Retrieved(AzString),
56 Deleted,
58 NotFound,
60 Denied,
63 Unavailable,
66 Error,
68}
69
70impl KeyringResult {
71 #[must_use] pub const fn secret(&self) -> Option<&AzString> {
73 match self {
74 Self::Retrieved(s) => Some(s),
75 _ => None,
76 }
77 }
78
79 #[must_use] pub const fn is_ok(&self) -> bool {
81 matches!(
82 self,
83 Self::Stored | Self::Retrieved(_) | Self::Deleted
84 )
85 }
86}
87
88impl_option!(
93 KeyringResult,
94 OptionKeyringResult,
95 copy = false,
96 [Debug, Clone, PartialEq, Eq]
97);
98
99#[cfg(test)]
100mod autotest_generated {
101 use super::*;
102 use alloc::string::String;
103
104 const NASTY_BYTES: &str = "pw\0with\u{1}nul\u{7f}\r\n\t";
107 const NASTY_UNICODE: &str = "🔑🗝 ключ 鍵 مفتاح e\u{301}\u{202e}terces\u{202c}\u{fffd}\u{10ffff}";
109
110 fn all_variants() -> [KeyringResult; 7] {
112 [
113 KeyringResult::Stored,
114 KeyringResult::Retrieved(AzString::from("s3cr3t")),
115 KeyringResult::Deleted,
116 KeyringResult::NotFound,
117 KeyringResult::Denied,
118 KeyringResult::Unavailable,
119 KeyringResult::Error,
120 ]
121 }
122
123 fn expected(r: &KeyringResult) -> (bool, bool) {
128 match r {
129 KeyringResult::Stored => (true, false),
130 KeyringResult::Retrieved(_) => (true, true),
131 KeyringResult::Deleted => (true, false),
132 KeyringResult::NotFound => (false, false),
133 KeyringResult::Denied => (false, false),
134 KeyringResult::Unavailable => (false, false),
135 KeyringResult::Error => (false, false),
136 }
137 }
138
139 #[test]
142 fn secret_returns_the_exact_payload_of_retrieved() {
143 let r = KeyringResult::Retrieved(AzString::from("hunter2"));
144 assert_eq!(r.secret().expect("Retrieved must yield a secret").as_str(), "hunter2");
145 }
146
147 #[test]
148 fn secret_is_none_for_every_non_retrieved_variant() {
149 for r in &all_variants() {
150 let has_secret = expected(r).1;
151 assert_eq!(
152 r.secret().is_some(),
153 has_secret,
154 "secret() disagrees with the documented contract for {r:?}"
155 );
156 }
157 }
158
159 #[test]
160 fn secret_of_an_empty_payload_is_some_empty_not_none() {
161 let r = KeyringResult::Retrieved(AzString::from(""));
164 assert_eq!(r.secret().expect("empty payload is still a payload").as_str(), "");
165 assert!(r.is_ok());
166
167 let d = KeyringResult::Retrieved(AzString::default());
168 assert_eq!(d.secret().expect("default payload is still a payload").as_str(), "");
169 assert!(d.is_ok());
170 }
171
172 #[test]
173 fn secret_preserves_interior_nul_and_control_bytes() {
174 let r = KeyringResult::Retrieved(AzString::from(NASTY_BYTES));
175 let s = r.secret().expect("Retrieved must yield a secret");
176 assert_eq!(s.as_str(), NASTY_BYTES);
177 assert_eq!(s.as_str().as_bytes(), NASTY_BYTES.as_bytes());
179 assert_eq!(s.as_str().len(), NASTY_BYTES.len());
180 }
181
182 #[test]
183 fn secret_preserves_multibyte_and_max_scalar_boundaries() {
184 let r = KeyringResult::Retrieved(AzString::from(NASTY_UNICODE));
185 let s = r.secret().expect("Retrieved must yield a secret");
186 assert_eq!(s.as_str(), NASTY_UNICODE);
189 assert_eq!(s.as_str().as_bytes(), NASTY_UNICODE.as_bytes());
190 assert_eq!(s.as_str().chars().count(), NASTY_UNICODE.chars().count());
191 assert!(s.as_str().ends_with('\u{10ffff}'));
192 }
193
194 #[test]
195 fn secret_handles_a_megabyte_payload() {
196 let big = "k".repeat(1 << 20);
197 let r = KeyringResult::Retrieved(AzString::from(big.clone()));
198 let s = r.secret().expect("Retrieved must yield a secret");
199 assert_eq!(s.as_str().len(), 1 << 20);
200 assert_eq!(s.as_str(), big.as_str());
201 assert!(r.is_ok());
202 }
203
204 #[test]
205 fn secret_borrows_the_payload_in_place_and_is_repeatable() {
206 let r = KeyringResult::Retrieved(AzString::from("borrow-me"));
207 let inner: &AzString = match &r {
208 KeyringResult::Retrieved(s) => s,
209 other => unreachable!("constructed Retrieved, got {other:?}"),
210 };
211
212 let first = r.secret().expect("Retrieved must yield a secret");
213 assert!(
214 core::ptr::eq(first, inner),
215 "secret() must borrow the payload, not hand back a copy"
216 );
217
218 let second = r.secret().expect("secret() must be repeatable");
220 assert!(core::ptr::eq(first, second));
221 assert_eq!(first.as_str().as_ptr(), second.as_str().as_ptr());
222 }
223
224 #[test]
227 fn is_ok_truth_table_is_exhaustive() {
228 for r in &all_variants() {
229 let want = expected(r).0;
230 assert_eq!(r.is_ok(), want, "is_ok() wrong for {r:?}");
231 }
232
233 assert!(KeyringResult::Stored.is_ok());
235 assert!(KeyringResult::Retrieved(AzString::from("x")).is_ok());
236 assert!(KeyringResult::Deleted.is_ok());
237 assert!(!KeyringResult::NotFound.is_ok());
238 assert!(!KeyringResult::Denied.is_ok());
239 assert!(!KeyringResult::Unavailable.is_ok());
240 assert!(!KeyringResult::Error.is_ok());
241 }
242
243 #[test]
244 fn is_ok_is_independent_of_the_retrieved_payload() {
245 let payloads = [
246 String::new(),
247 String::from(NASTY_BYTES),
248 String::from(NASTY_UNICODE),
249 "a".repeat(100_000),
250 ];
251 for p in payloads {
252 assert!(
253 KeyringResult::Retrieved(AzString::from(p)).is_ok(),
254 "Retrieved is a success outcome whatever it carries"
255 );
256 }
257 }
258
259 #[test]
260 fn is_ok_and_secret_never_contradict_each_other() {
261 for r in &all_variants() {
262 if r.secret().is_some() {
264 assert!(r.is_ok(), "{r:?} yielded a secret but reports failure");
265 }
266 if !r.is_ok() {
267 assert!(r.secret().is_none(), "failure {r:?} yielded a secret");
268 }
269 }
270 }
271
272 #[test]
273 fn accessors_are_pure() {
274 for r in &all_variants() {
275 let before = r.clone();
276 for _ in 0..8 {
277 assert_eq!(r.is_ok(), before.is_ok());
278 assert_eq!(r.secret().is_some(), before.secret().is_some());
279 }
280 assert_eq!(*r, before, "accessors must not mutate the receiver");
281 }
282 }
283
284 #[test]
287 fn dropping_a_clone_leaves_the_original_payload_readable() {
288 let payload = "🔐 secret-that-must-survive";
289 let original = KeyringResult::Retrieved(AzString::from(payload));
290
291 for _ in 0..1000 {
292 let c = original.clone();
293 assert_eq!(c.secret().expect("clone keeps the payload").as_str(), payload);
294 drop(c);
296 }
297
298 assert_eq!(
299 original.secret().expect("original survives 1000 clone/drop cycles").as_str(),
300 payload
301 );
302 assert!(original.is_ok());
303 }
304
305 #[test]
306 fn dropping_the_original_leaves_the_clone_payload_readable() {
307 let payload = NASTY_UNICODE;
308 let original = KeyringResult::Retrieved(AzString::from(payload));
309 let clone = original.clone();
310 drop(original);
311 assert_eq!(clone.secret().expect("clone owns its payload").as_str(), payload);
312 }
313
314 #[test]
315 fn eq_is_sensitive_to_both_variant_and_payload() {
316 assert_eq!(
317 KeyringResult::Retrieved(AzString::from("a")),
318 KeyringResult::Retrieved(AzString::from("a"))
319 );
320 assert_ne!(
321 KeyringResult::Retrieved(AzString::from("a")),
322 KeyringResult::Retrieved(AzString::from("b"))
323 );
324 assert_ne!(KeyringResult::Retrieved(AzString::from("")), KeyringResult::Stored);
326 assert_ne!(KeyringResult::NotFound, KeyringResult::Denied);
327 assert_ne!(KeyringResult::Unavailable, KeyringResult::Error);
328 assert_eq!(KeyringResult::Deleted, KeyringResult::Deleted);
329 }
330
331 #[test]
334 fn option_wrapper_round_trips_none_and_every_variant() {
335 let none: OptionKeyringResult = Option::<KeyringResult>::None.into();
336 assert!(none.is_none());
337 assert_eq!(Option::<KeyringResult>::from(none), None);
338 assert!(OptionKeyringResult::default().is_none());
339
340 for r in all_variants() {
341 let wrapped: OptionKeyringResult = Some(r.clone()).into();
342 let back: Option<KeyringResult> = wrapped.into();
343 assert_eq!(back, Some(r), "encode/decode through OptionKeyringResult lost data");
344 }
345 }
346
347 #[test]
348 fn option_wrapper_accessors_agree_with_each_other() {
349 let payload = "round-trip-me";
350 let some = OptionKeyringResult::Some(KeyringResult::Retrieved(AzString::from(payload)));
351 assert!(some.is_some());
352 assert!(!some.is_none());
353 assert_eq!(
354 some.as_option().and_then(KeyringResult::secret).map(AzString::as_str),
355 Some(payload)
356 );
357 assert_eq!(some.as_ref(), some.as_option());
358
359 let none = OptionKeyringResult::None;
360 assert!(none.is_none());
361 assert!(!none.is_some());
362 assert!(none.as_option().is_none());
363 }
364
365 #[test]
366 fn option_wrapper_into_option_clones_and_leaves_the_wrapper_intact() {
367 let payload = NASTY_BYTES;
370 let wrapped = OptionKeyringResult::Some(KeyringResult::Retrieved(AzString::from(payload)));
371
372 let first = wrapped.into_option().expect("Some stays Some");
373 let second = wrapped.into_option().expect("into_option must not consume the wrapper");
374 assert_eq!(first, second);
375 assert_eq!(first.secret().expect("payload survives").as_str(), payload);
376 drop(first);
377 assert_eq!(second.secret().expect("payload survives its sibling").as_str(), payload);
378 drop(second);
379 assert_eq!(
380 wrapped.as_option().and_then(KeyringResult::secret).map(AzString::as_str),
381 Some(payload),
382 "wrapper must still own its payload after both clones died"
383 );
384 }
385
386 #[test]
387 fn option_wrapper_replace_returns_the_previous_value() {
388 let mut o = OptionKeyringResult::None;
389 assert_eq!(o.replace(KeyringResult::Stored), OptionKeyringResult::None);
390
391 let prev = o.replace(KeyringResult::Retrieved(AzString::from("new")));
392 assert_eq!(prev, OptionKeyringResult::Some(KeyringResult::Stored));
393 assert_eq!(
394 o.as_option().and_then(KeyringResult::secret).map(AzString::as_str),
395 Some("new")
396 );
397 }
398
399 #[test]
400 fn option_wrapper_as_mut_can_overwrite_a_retrieved_payload() {
401 let mut o = OptionKeyringResult::Some(KeyringResult::Retrieved(AzString::from("old")));
402 if let Some(slot) = o.as_mut() {
403 *slot = KeyringResult::Deleted; }
405 assert_eq!(o.as_option(), Some(&KeyringResult::Deleted));
406 assert!(o.as_option().expect("still Some").is_ok());
407 assert!(o.as_option().expect("still Some").secret().is_none());
408 }
409
410 #[test]
413 fn request_equality_is_sensitive_to_key_secret_and_biometry_flag() {
414 let store = KeyringRequest::Store {
415 key: AzString::from("k"),
416 secret: AzString::from("s"),
417 require_biometry: false,
418 };
419 assert_eq!(store, store.clone());
420
421 assert_ne!(
422 store,
423 KeyringRequest::Store {
424 key: AzString::from("k"),
425 secret: AzString::from("s"),
426 require_biometry: true,
427 },
428 "require_biometry changes the stored access control — it must not be ignored"
429 );
430 assert_ne!(
431 store,
432 KeyringRequest::Store {
433 key: AzString::from("k"),
434 secret: AzString::from("other"),
435 require_biometry: false,
436 }
437 );
438 assert_ne!(
439 KeyringRequest::Get { key: AzString::from("k") },
440 KeyringRequest::Delete { key: AzString::from("k") },
441 "a read and a delete of the same key are different operations"
442 );
443 assert_ne!(
444 KeyringRequest::Get { key: AzString::from("a") },
445 KeyringRequest::Get { key: AzString::from("b") }
446 );
447 }
448
449 #[test]
450 fn request_payloads_survive_clone_and_drop() {
451 let key = "🔑".repeat(10_000);
452 let req = KeyringRequest::Store {
453 key: AzString::from(key.clone()),
454 secret: AzString::from(NASTY_BYTES),
455 require_biometry: true,
456 };
457
458 let clone = req.clone();
459 drop(req);
460
461 match &clone {
462 KeyringRequest::Store { key: k, secret: s, require_biometry } => {
463 assert_eq!(k.as_str(), key.as_str());
464 assert_eq!(s.as_str(), NASTY_BYTES);
465 assert!(*require_biometry);
466 }
467 other => unreachable!("constructed Store, got {other:?}"),
468 }
469 }
470
471 #[test]
472 fn request_accepts_empty_keys_without_panicking() {
473 let empty = KeyringRequest::Get { key: AzString::from("") };
476 assert_eq!(empty, KeyringRequest::Get { key: AzString::default() });
477 assert_ne!(empty, KeyringRequest::Get { key: AzString::from("k") });
478 }
479}