1use serde::{Deserialize, Serialize};
35
36use crate::storage::{MlsStorage, StorageError};
37use crate::GroupId;
38
39pub const CONFORMANCE_KEY_TYPE: &str = "mls_storage_conformance_probe";
44
45pub const CONFORMANCE_KEY_TYPE_OTHER: &str = "mls_storage_conformance_probe_other";
47
48const CONFORMANCE_KEY_TYPE_UNUSED: &str = "mls_storage_conformance_probe_unused";
51
52const CONFORMANCE_KEY_TYPE_SUFFIXED: &str = "suffixed_mls_storage_conformance_probe";
59
60const ISOLATION_PROBE_ID: &str = "mls_storage_conformance_isolation";
64
65const ISOLATION_LISTING_PROBE_ID: &str = "mls_storage_conformance_isolation_listing";
68
69pub const LARGE_RECORD_BYTES: usize = 512 * 1024;
78
79const CONCURRENT_WRITERS: usize = 8;
81const WRITES_PER_WRITER: usize = 16;
83const CONCURRENT_VALUE_BYTES: usize = 4096;
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ConformanceFailure {
90 pub check: String,
92 pub detail: String,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct ConformanceReport {
99 pub passed: Vec<String>,
101 pub failures: Vec<ConformanceFailure>,
103}
104
105impl ConformanceReport {
106 pub fn new() -> Self {
108 Self::default()
109 }
110
111 pub fn is_green(&self) -> bool {
113 self.failures.is_empty()
114 }
115
116 pub fn summary(&self) -> String {
118 if self.is_green() {
119 format!("{} checks passed", self.passed.len())
120 } else {
121 let names: Vec<&str> = self
122 .failures
123 .iter()
124 .map(|failure| failure.check.as_str())
125 .collect();
126 format!(
127 "{} of {} checks failed: {}",
128 self.failures.len(),
129 self.passed.len() + self.failures.len(),
130 names.join(", ")
131 )
132 }
133 }
134
135 pub fn pass(&mut self, check: &str) {
137 self.passed.push(check.to_string());
138 }
139
140 pub fn fail(&mut self, check: &str, detail: impl Into<String>) {
142 self.failures.push(ConformanceFailure {
143 check: check.to_string(),
144 detail: detail.into(),
145 });
146 }
147
148 pub fn check(&mut self, name: &str, outcome: Result<(), String>) {
150 match outcome {
151 Ok(()) => self.pass(name),
152 Err(detail) => self.fail(name, detail),
153 }
154 }
155}
156
157fn expect_load(
158 storage: &dyn MlsStorage,
159 key_type: &str,
160 key_id: &str,
161 expected: &[u8],
162) -> Result<(), String> {
163 match storage.load(key_type, key_id) {
164 Ok(Some(actual)) if actual == expected => Ok(()),
165 Ok(Some(actual)) => Err(format!(
166 "loaded {} bytes, expected {} (contents differ)",
167 actual.len(),
168 expected.len()
169 )),
170 Ok(None) => Err("stored value loaded back as missing".to_string()),
171 Err(err) => Err(format!("load failed: {err}")),
172 }
173}
174
175fn store_and_read_back(
176 storage: &dyn MlsStorage,
177 key_id: &str,
178 value: &[u8],
179 what: &str,
180) -> Result<(), String> {
181 storage
182 .store(CONFORMANCE_KEY_TYPE, key_id, value)
183 .map_err(|err| format!("store of {what} failed: {err}"))?;
184 expect_load(storage, CONFORMANCE_KEY_TYPE, key_id, value)
185 .map_err(|detail| format!("{what}: {detail}"))
186}
187
188pub fn run(storage: &dyn MlsStorage) -> ConformanceReport {
194 let mut report = ConformanceReport::new();
195
196 let isolation = key_types_are_isolated(storage);
206 let isolated = isolation.is_ok();
207 report.check("key_types_are_separate_namespaces", isolation);
208 if !isolated {
209 return report;
210 }
211
212 cleanup(storage);
215
216 report.check(
218 "store_then_load",
219 store_and_read_back(storage, "basic", b"hello", "a value"),
220 );
221
222 let binary: Vec<u8> = (0u8..=255).collect();
226 report.check(
227 "binary_values_round_trip",
228 store_and_read_back(storage, "binary", &binary, "a binary value"),
229 );
230
231 report.check(
233 "empty_value_is_not_missing",
234 store_and_read_back(storage, "empty", b"", "an empty value"),
235 );
236
237 let outcome = storage
241 .store(CONFORMANCE_KEY_TYPE, "overwrite", b"first")
242 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, "overwrite", b"second"))
243 .map_err(|err| format!("store failed: {err}"))
244 .and_then(|()| expect_load(storage, CONFORMANCE_KEY_TYPE, "overwrite", b"second"));
245 report.check("store_overwrites", outcome);
246
247 let outcome = match storage.load(CONFORMANCE_KEY_TYPE, "never_written") {
251 Ok(None) => Ok(()),
252 Ok(Some(_)) => Err("a key that was never written returned a value".to_string()),
253 Err(StorageError::KeyNotFound(_)) => {
254 Err("absent keys must load as Ok(None), not KeyNotFound".to_string())
255 }
256 Err(err) => Err(format!("load of an absent key failed: {err}")),
257 };
258 report.check("absent_key_loads_as_none", outcome);
259
260 let outcome = storage
264 .store(CONFORMANCE_KEY_TYPE, "deletable", b"x")
265 .and_then(|()| storage.delete(CONFORMANCE_KEY_TYPE, "deletable"))
266 .map_err(|err| format!("store/delete failed: {err}"))
267 .and_then(|()| match storage.load(CONFORMANCE_KEY_TYPE, "deletable") {
268 Ok(None) => Ok(()),
269 Ok(Some(_)) => Err("value survived a delete".to_string()),
270 Err(err) => Err(format!("load after delete failed: {err}")),
271 })
272 .and_then(
273 |()| match storage.delete(CONFORMANCE_KEY_TYPE, "deletable") {
274 Ok(()) => Ok(()),
275 Err(err) => Err(format!("second delete must succeed, got: {err}")),
276 },
277 );
278 report.check("delete_removes_and_is_idempotent", outcome);
279
280 let outcome = (|| -> Result<(), String> {
284 storage
285 .store(CONFORMANCE_KEY_TYPE, "shared_id", b"mine")
286 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs"))
287 .map_err(|err| format!("store failed: {err}"))?;
288 let keys = storage
289 .list_keys(CONFORMANCE_KEY_TYPE)
290 .map_err(|err| format!("list_keys failed: {err}"))?;
291 for expected in ["basic", "binary", "empty", "overwrite", "shared_id"] {
292 if !keys.iter().any(|key| key == expected) {
293 return Err(format!("list_keys omitted {expected:?}"));
294 }
295 }
296 if keys.iter().any(|key| key == "deletable") {
297 return Err("list_keys named a deleted record".to_string());
298 }
299 let other = storage
300 .list_keys(CONFORMANCE_KEY_TYPE_OTHER)
301 .map_err(|err| format!("list_keys failed: {err}"))?;
302 if other.len() != 1 || other.first().map(String::as_str) != Some("shared_id") {
303 return Err(format!("list_keys leaked across key types: {other:?}"));
304 }
305 Ok(())
306 })();
307 report.check("list_keys_is_accurate_and_scoped", outcome);
308
309 let outcome = match storage.list_keys(CONFORMANCE_KEY_TYPE_UNUSED) {
312 Ok(keys) if keys.is_empty() => Ok(()),
313 Ok(keys) => Err(format!("an unused key type listed {} keys", keys.len())),
314 Err(err) => Err(format!("list_keys on an unused key type failed: {err}")),
315 };
316 report.check("unused_key_type_lists_empty", outcome);
317
318 let session_id = "session:off1qq6k7tx3adkz9gkvwt6c5c4v8lkh5g66ru6jm3s0dm\
327 :off1qq7wtkw2pmld2pcj3zn5fvhhj3kj5sgnlwq0mnh2yg7d";
328 let ids = [
329 ("a key package id", "8f2c4d3a-4b6e-4c1f-9d0a-1e2f3a4b5c6d"),
330 (
331 "an OpenMLS record id",
332 "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
333 ),
334 ("a session id", session_id),
335 ];
336 let outcome = (|| -> Result<(), String> {
337 for (what, id) in ids {
338 store_and_read_back(storage, id, what.as_bytes(), what)?;
339 }
340 let keys = storage
341 .list_keys(CONFORMANCE_KEY_TYPE)
342 .map_err(|err| format!("list_keys failed: {err}"))?;
343 for (what, id) in ids {
344 if !keys.iter().any(|key| key == id) {
345 return Err(format!("list_keys did not return {what} verbatim"));
346 }
347 }
348 Ok(())
349 })();
350 report.check("key_ids_as_the_sdk_writes_them", outcome);
351
352 let exact_ids: [(&str, &[u8]); 7] = [
359 ("group:Team", b"upper case"),
360 ("group:team", b"lower case"),
361 ("group:caf\u{e9}", b"composed"),
362 ("group:cafe\u{301}", b"decomposed"),
363 ("group:CON", b"a reserved device name"),
364 ("group:a*b?", b"wildcards"),
365 ("group:abc.", b"a trailing dot"),
366 ];
367 let outcome = (|| -> Result<(), String> {
368 for (id, _) in exact_ids {
369 if GroupId::new(id).is_err() {
370 return Err(format!("{id:?} is not a valid group id; fix the suite"));
371 }
372 }
373 for (id, value) in exact_ids {
374 storage
375 .store(CONFORMANCE_KEY_TYPE, id, value)
376 .map_err(|err| format!("store of {id:?} failed: {err}"))?;
377 }
378 for (id, value) in exact_ids {
379 expect_load(storage, CONFORMANCE_KEY_TYPE, id, value)
380 .map_err(|detail| format!("{id:?}: {detail}"))?;
381 }
382 let keys = storage
383 .list_keys(CONFORMANCE_KEY_TYPE)
384 .map_err(|err| format!("list_keys failed: {err}"))?;
385 for (id, _) in exact_ids {
386 if !keys.iter().any(|key| key == id) {
387 return Err(format!("list_keys did not return {id:?} verbatim"));
388 }
389 }
390 Ok(())
391 })();
392 report.check("key_ids_are_byte_exact", outcome);
393
394 let long_key = format!(
400 "session:{}",
401 "a".repeat(GroupId::MAX_LEN - "session:".len())
402 );
403 let outcome = (|| -> Result<(), String> {
404 if GroupId::new(long_key.as_str()).is_err() {
405 return Err("the probe id is not a valid group id; fix the suite".to_string());
406 }
407 store_and_read_back(storage, &long_key, b"long", "a maximum-length key id")
408 })();
409 report.check("long_key_ids_round_trip", outcome);
410
411 let large = vec![0xa5u8; LARGE_RECORD_BYTES];
416 report.check(
417 "large_records_round_trip",
418 store_and_read_back(storage, "large", &large, "a large record"),
419 );
420
421 let outcome = (|| -> Result<(), String> {
427 let describe = |err: StorageError| format!("exists failed: {err}");
428 if storage
429 .exists(CONFORMANCE_KEY_TYPE, "exists_probe")
430 .map_err(describe)?
431 {
432 return Err("exists reported a key that was never written".to_string());
433 }
434 storage
435 .store(CONFORMANCE_KEY_TYPE, "exists_probe", b"x")
436 .map_err(|err| format!("store failed: {err}"))?;
437 if !storage
438 .exists(CONFORMANCE_KEY_TYPE, "exists_probe")
439 .map_err(describe)?
440 {
441 return Err("exists missed a key that was just stored".to_string());
442 }
443 if storage
444 .exists(CONFORMANCE_KEY_TYPE_OTHER, "exists_probe")
445 .map_err(describe)?
446 {
447 return Err("exists found a key under the wrong key type".to_string());
448 }
449 storage
450 .delete(CONFORMANCE_KEY_TYPE, "exists_probe")
451 .map_err(|err| format!("delete failed: {err}"))?;
452 if storage
453 .exists(CONFORMANCE_KEY_TYPE, "exists_probe")
454 .map_err(describe)?
455 {
456 return Err("exists reported a deleted key".to_string());
457 }
458 Ok(())
459 })();
460 report.check("exists_agrees_with_load", outcome);
461
462 let outcome = (|| -> Result<(), String> {
469 storage
470 .store(CONFORMANCE_KEY_TYPE_OTHER, "survivor", b"kept")
471 .map_err(|err| format!("store failed: {err}"))?;
472 storage
473 .clear_type(CONFORMANCE_KEY_TYPE)
474 .map_err(|err| format!("clear_type failed: {err}"))?;
475 let cleared = storage
476 .list_keys(CONFORMANCE_KEY_TYPE)
477 .map_err(|err| format!("list_keys failed: {err}"))?;
478 if !cleared.is_empty() {
479 return Err(format!(
480 "{} records survived clear_type on their own key type",
481 cleared.len()
482 ));
483 }
484 expect_load(storage, CONFORMANCE_KEY_TYPE_OTHER, "survivor", b"kept")
485 .map_err(|_| "clear_type reached into another key type".to_string())?;
486 expect_load(storage, CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs")
487 .map_err(|_| "clear_type reached into another key type".to_string())?;
488 storage
489 .clear_type(CONFORMANCE_KEY_TYPE)
490 .map_err(|err| format!("clear_type on an emptied key type failed: {err}"))?;
491 storage
492 .clear_type(CONFORMANCE_KEY_TYPE_UNUSED)
493 .map_err(|err| format!("clear_type on an unused key type failed: {err}"))?;
494 Ok(())
495 })();
496 report.check("clear_type_is_scoped_and_idempotent", outcome);
497
498 report.check("concurrent_writes_land_whole", concurrent_writes(storage));
504
505 let outcome = (|| -> Result<(), String> {
508 cleanup(storage);
509 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
510 let remaining = storage
511 .list_keys(key_type)
512 .map_err(|err| format!("list_keys failed: {err}"))?;
513 if !remaining.is_empty() {
514 return Err(format!(
515 "{} records survived a listed delete in {key_type}",
516 remaining.len()
517 ));
518 }
519 }
520 Ok(())
521 })();
522 report.check("listed_records_can_all_be_deleted", outcome);
523
524 report
525}
526
527fn key_types_are_isolated(storage: &dyn MlsStorage) -> Result<(), String> {
536 let outcome = (|| -> Result<(), String> {
537 storage
538 .store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID, b"theirs")
539 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID, b"mine"))
540 .and_then(|()| {
541 storage.store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID, b"x")
542 })
543 .and_then(|()| {
544 storage.store(CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID, b"x")
545 })
546 .map_err(|err| {
547 format!("store failed, so isolation could not be checked and nothing was deleted: {err}")
548 })?;
549 if let Ok(Some(other)) = storage.load(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID) {
550 if other == b"mine" {
551 return Err(
552 "the same key id in two key types collided: a write under one \
553 replaced the other"
554 .to_string(),
555 );
556 }
557 }
558 if let Ok(listed) = storage.list_keys(CONFORMANCE_KEY_TYPE) {
559 if listed.iter().any(|key| key == ISOLATION_LISTING_PROBE_ID) {
560 return Err("list_keys named a record written under another key type".to_string());
561 }
562 }
563 Ok(())
564 })();
565 for (key_type, key_id) in [
566 (CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID),
567 (CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID),
568 (CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID),
569 (CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID),
570 ] {
571 let _ = storage.delete(key_type, key_id);
572 }
573 outcome
574}
575
576fn concurrent_writes(storage: &dyn MlsStorage) -> Result<(), String> {
590 let shared_values: Vec<Vec<u8>> = (0..CONCURRENT_WRITERS)
591 .map(|writer| vec![writer as u8 + 1; CONCURRENT_VALUE_BYTES])
592 .collect();
593
594 #[cfg(not(all(target_family = "wasm", not(target_feature = "atomics"))))]
595 let outcomes: Vec<Result<(), String>> = std::thread::scope(|scope| {
596 let handles: Vec<_> = (0..CONCURRENT_WRITERS)
597 .map(|writer| {
598 let shared_values = &shared_values;
599 std::thread::Builder::new().spawn_scoped(scope, move || -> Result<(), String> {
600 for round in 0..WRITES_PER_WRITER {
601 write_round(storage, shared_values, writer, round)?;
602 }
603 check_own_key(storage, writer)
604 })
605 })
606 .collect();
607 handles
608 .into_iter()
609 .map(|spawned| match spawned {
610 Ok(handle) => handle.join().unwrap_or_else(|_| {
611 Err("a writer thread panicked inside the adapter".to_string())
612 }),
613 Err(err) => Err(format!("could not start a writer thread: {err}")),
614 })
615 .collect()
616 });
617 #[cfg(all(target_family = "wasm", not(target_feature = "atomics")))]
618 let outcomes: Vec<Result<(), String>> = {
619 let mut outcomes = Vec::with_capacity(CONCURRENT_WRITERS + 1);
620 outcomes.push((|| {
621 for round in 0..WRITES_PER_WRITER {
622 for writer in 0..CONCURRENT_WRITERS {
623 write_round(storage, &shared_values, writer, round)?;
624 }
625 }
626 Ok(())
627 })());
628 outcomes.extend((0..CONCURRENT_WRITERS).map(|writer| check_own_key(storage, writer)));
629 outcomes
630 };
631 for outcome in outcomes {
632 outcome?;
633 }
634
635 match storage.load(CONFORMANCE_KEY_TYPE, CONCURRENT_SHARED_KEY) {
636 Ok(Some(seen)) if shared_values.contains(&seen) => Ok(()),
637 Ok(Some(seen)) => Err(format!(
638 "after all writers finished the shared key held a torn value ({} bytes)",
639 seen.len()
640 )),
641 Ok(None) => Err("after all writers finished the shared key was missing".to_string()),
642 Err(err) => Err(format!("load after concurrent writes failed: {err}")),
643 }
644}
645
646const CONCURRENT_SHARED_KEY: &str = "concurrent_shared";
648
649fn concurrent_own_value(writer: usize, round: usize) -> Vec<u8> {
651 let mut value = vec![writer as u8 + 1; CONCURRENT_VALUE_BYTES];
652 value[0] = round as u8;
653 value
654}
655
656fn write_round(
659 storage: &dyn MlsStorage,
660 shared_values: &[Vec<u8>],
661 writer: usize,
662 round: usize,
663) -> Result<(), String> {
664 storage
665 .store(
666 CONFORMANCE_KEY_TYPE,
667 &format!("concurrent_writer_{writer}"),
668 &concurrent_own_value(writer, round),
669 )
670 .map_err(|err| format!("writer {writer}: store failed: {err}"))?;
671 storage
672 .store(
673 CONFORMANCE_KEY_TYPE,
674 CONCURRENT_SHARED_KEY,
675 &shared_values[writer],
676 )
677 .map_err(|err| format!("writer {writer}: store failed: {err}"))?;
678 match storage.load(CONFORMANCE_KEY_TYPE, CONCURRENT_SHARED_KEY) {
679 Ok(Some(seen)) if shared_values.contains(&seen) => Ok(()),
680 Ok(Some(seen)) => Err(format!(
681 "writer {writer} read a torn value ({} bytes) from a key under concurrent overwrite",
682 seen.len()
683 )),
684 Ok(None) => Err(format!(
685 "writer {writer} found a key it had just written missing under concurrent overwrite"
686 )),
687 Err(err) => Err(format!(
688 "writer {writer}: load under concurrent writes failed: {err}"
689 )),
690 }
691}
692
693fn check_own_key(storage: &dyn MlsStorage, writer: usize) -> Result<(), String> {
695 expect_load(
696 storage,
697 CONFORMANCE_KEY_TYPE,
698 &format!("concurrent_writer_{writer}"),
699 &concurrent_own_value(writer, WRITES_PER_WRITER - 1),
700 )
701 .map_err(|detail| format!("writer {writer}'s own key: {detail}"))
702}
703
704fn cleanup(storage: &dyn MlsStorage) {
706 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
707 if let Ok(keys) = storage.list_keys(key_type) {
708 for key in keys {
709 let _ = storage.delete(key_type, &key);
710 }
711 }
712 }
713}
714
715pub fn run_json(storage: &dyn MlsStorage) -> String {
720 let report = run(storage);
721 serde_json::to_string(&report).unwrap_or_else(|err| {
722 format!(
723 "{{\"passed\":[],\"failures\":[{{\"check\":\"report_serialization\",\"detail\":\"{}\"}}]}}",
724 err.to_string().replace('"', "'")
725 )
726 })
727}
728
729#[cfg(test)]
730mod tests {
731 use std::collections::HashMap;
732 use std::sync::Mutex;
733
734 use super::*;
735 use crate::storage::{InMemoryStorage, StorageResult};
736
737 #[test]
738 fn the_suite_is_green_against_the_in_memory_store() {
739 let storage = InMemoryStorage::new();
740 let report = run(&storage);
741 assert!(report.is_green(), "{}", report.summary());
742 assert_eq!(report.passed.len(), 17, "{:?}", report.passed);
743 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
744 assert!(
745 storage.list_keys(key_type).unwrap().is_empty(),
746 "the suite left records behind in {key_type}"
747 );
748 }
749 }
750
751 #[test]
752 fn the_suite_leaves_records_outside_its_probe_key_types_alone() {
753 let storage = InMemoryStorage::new();
754 storage.store("identity", "key_pair", b"secret").unwrap();
755 let report = run(&storage);
756 assert!(report.is_green(), "{}", report.summary());
757 assert_eq!(
758 storage.load("identity", "key_pair").unwrap(),
759 Some(b"secret".to_vec())
760 );
761 assert_eq!(storage.list_keys("identity").unwrap(), vec!["key_pair"]);
762 }
763
764 #[test]
765 fn the_report_renders_the_documented_json_shape() {
766 let json = run_json(&InMemoryStorage::new());
767 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
768 assert!(parsed["passed"].as_array().is_some_and(|p| p.len() == 17));
769 assert!(parsed["failures"].as_array().is_some_and(Vec::is_empty));
770
771 let mut report = ConformanceReport::new();
772 report.fail("x", "why");
773 let parsed: serde_json::Value =
774 serde_json::from_str(&serde_json::to_string(&report).unwrap()).unwrap();
775 assert_eq!(parsed["failures"][0]["check"], "x");
776 assert_eq!(parsed["failures"][0]["detail"], "why");
777 }
778
779 #[derive(Debug, Clone, Copy)]
782 enum Fault {
783 None,
784 CorruptsValues,
785 StringTypedValues,
786 EmptyReadsAsMissing,
787 KeepsFirstWrite,
788 AbsentIsAnError,
789 DeleteOfAbsentFails,
790 IgnoresKeyType,
791 ListsBySuffix,
792 ListReturnsFirstPageOnly,
793 ListOfUnknownTypeFails,
794 KeepsIdUpToFirstColon,
795 CaseFoldsKeyIds,
796 CapsKeyIdAt255,
797 TruncatesValuesAt64KiB,
798 ExistsAlwaysFalse,
799 ClearTypeClearsEverything,
800 TearsOverwrites,
801 DeleteIsANoOp,
802 }
803
804 struct Faulty {
805 fault: Fault,
806 records: Mutex<HashMap<(String, String), Vec<u8>>>,
807 previous: Mutex<HashMap<(String, String), Vec<u8>>>,
808 }
809
810 impl Faulty {
811 fn new(fault: Fault) -> Self {
812 Self {
813 fault,
814 records: Mutex::default(),
815 previous: Mutex::default(),
816 }
817 }
818
819 fn key(&self, key_type: &str, key_id: &str) -> (String, String) {
820 let key_type = match self.fault {
821 Fault::IgnoresKeyType => "",
822 _ => key_type,
823 };
824 let key_id = match self.fault {
825 Fault::KeepsIdUpToFirstColon => key_id.split(':').next().unwrap_or("").to_string(),
826 Fault::CaseFoldsKeyIds => key_id.to_lowercase(),
827 _ => key_id.to_string(),
828 };
829 (key_type.to_string(), key_id)
830 }
831 }
832
833 impl MlsStorage for Faulty {
834 fn store(&self, key_type: &str, key_id: &str, data: &[u8]) -> StorageResult<()> {
835 if matches!(self.fault, Fault::CapsKeyIdAt255) && key_id.len() > 255 {
836 return Err(StorageError::StoreFailed("key too long".to_string()));
837 }
838 let mut data = data.to_vec();
839 if matches!(self.fault, Fault::TruncatesValuesAt64KiB) {
840 data.truncate(64 * 1024);
841 }
842 if matches!(self.fault, Fault::StringTypedValues) {
843 data = String::from_utf8_lossy(&data).into_owned().into_bytes();
844 }
845 let key = self.key(key_type, key_id);
846 let mut records = self.records.lock().unwrap();
847 if matches!(self.fault, Fault::KeepsFirstWrite) {
848 records.entry(key).or_insert(data);
849 return Ok(());
850 }
851 if let Some(old) = records.insert(key.clone(), data) {
852 self.previous.lock().unwrap().insert(key, old);
853 }
854 Ok(())
855 }
856
857 fn load(&self, key_type: &str, key_id: &str) -> StorageResult<Option<Vec<u8>>> {
858 let key = self.key(key_type, key_id);
859 let current = self.records.lock().unwrap().get(&key).cloned();
860 match (self.fault, current) {
861 (Fault::AbsentIsAnError, None) => {
862 Err(StorageError::KeyNotFound(key_id.to_string()))
863 }
864 (Fault::EmptyReadsAsMissing, Some(current)) if current.is_empty() => Ok(None),
865 (Fault::CorruptsValues, Some(mut current)) if !current.is_empty() => {
866 current[0] ^= 0xff;
867 Ok(Some(current))
868 }
869 (Fault::TearsOverwrites, Some(current)) => {
870 let previous = self.previous.lock().unwrap().get(&key).cloned();
873 match previous {
874 Some(previous)
875 if previous != current && previous.len() == current.len() =>
876 {
877 let half = current.len() / 2;
878 let mut torn = previous[..half].to_vec();
879 torn.extend_from_slice(¤t[half..]);
880 Ok(Some(torn))
881 }
882 _ => Ok(Some(current)),
883 }
884 }
885 (_, current) => Ok(current),
886 }
887 }
888
889 fn delete(&self, key_type: &str, key_id: &str) -> StorageResult<()> {
890 if matches!(self.fault, Fault::DeleteIsANoOp) {
891 return Ok(());
892 }
893 let key = self.key(key_type, key_id);
894 let removed = self.records.lock().unwrap().remove(&key);
895 if removed.is_none() && matches!(self.fault, Fault::DeleteOfAbsentFails) {
896 return Err(StorageError::KeyNotFound(key_id.to_string()));
897 }
898 Ok(())
899 }
900
901 fn list_keys(&self, key_type: &str) -> StorageResult<Vec<String>> {
902 let (key_type, _) = self.key(key_type, "");
903 let keys: Vec<String> = self
904 .records
905 .lock()
906 .unwrap()
907 .keys()
908 .filter(|(kt, _)| match self.fault {
909 Fault::ListsBySuffix => kt.ends_with(key_type.as_str()),
912 _ => *kt == key_type,
913 })
914 .map(|(_, id)| id.clone())
915 .collect();
916 match self.fault {
917 Fault::ListReturnsFirstPageOnly => Ok(keys.into_iter().take(4).collect()),
919 Fault::ListOfUnknownTypeFails if keys.is_empty() => Err(StorageError::LoadFailed(
922 format!("no such key type: {key_type}"),
923 )),
924 _ => Ok(keys),
925 }
926 }
927
928 fn exists(&self, key_type: &str, key_id: &str) -> StorageResult<bool> {
929 if matches!(self.fault, Fault::ExistsAlwaysFalse) {
930 return Ok(false);
931 }
932 Ok(self.load(key_type, key_id)?.is_some())
933 }
934
935 fn clear_type(&self, key_type: &str) -> StorageResult<()> {
936 let mut records = self.records.lock().unwrap();
937 if matches!(self.fault, Fault::ClearTypeClearsEverything) {
938 records.clear();
939 return Ok(());
940 }
941 let (key_type, _) = self.key(key_type, "");
942 records.retain(|(kt, _), _| *kt != key_type);
943 Ok(())
944 }
945 }
946
947 #[test]
948 fn the_fault_injecting_store_is_green_with_no_fault() {
949 let report = run(&Faulty::new(Fault::None));
951 assert!(report.is_green(), "{}", report.summary());
952 }
953
954 #[test]
955 fn the_suite_catches_each_defect_class() {
956 let cases = [
959 (Fault::IgnoresKeyType, "key_types_are_separate_namespaces"),
960 (Fault::ListsBySuffix, "key_types_are_separate_namespaces"),
961 (Fault::CorruptsValues, "store_then_load"),
962 (Fault::StringTypedValues, "binary_values_round_trip"),
963 (Fault::EmptyReadsAsMissing, "empty_value_is_not_missing"),
964 (Fault::KeepsFirstWrite, "store_overwrites"),
965 (Fault::AbsentIsAnError, "absent_key_loads_as_none"),
966 (
967 Fault::DeleteOfAbsentFails,
968 "delete_removes_and_is_idempotent",
969 ),
970 (
971 Fault::ListReturnsFirstPageOnly,
972 "list_keys_is_accurate_and_scoped",
973 ),
974 (Fault::ListOfUnknownTypeFails, "unused_key_type_lists_empty"),
975 (
976 Fault::KeepsIdUpToFirstColon,
977 "key_ids_as_the_sdk_writes_them",
978 ),
979 (Fault::CaseFoldsKeyIds, "key_ids_are_byte_exact"),
980 (Fault::CapsKeyIdAt255, "long_key_ids_round_trip"),
981 (Fault::TruncatesValuesAt64KiB, "large_records_round_trip"),
982 (Fault::ExistsAlwaysFalse, "exists_agrees_with_load"),
983 (
984 Fault::ClearTypeClearsEverything,
985 "clear_type_is_scoped_and_idempotent",
986 ),
987 (Fault::TearsOverwrites, "concurrent_writes_land_whole"),
988 (Fault::DeleteIsANoOp, "listed_records_can_all_be_deleted"),
989 ];
990 let covered: std::collections::BTreeSet<&str> =
993 cases.iter().map(|(_, check)| *check).collect();
994 let checks: std::collections::BTreeSet<String> =
995 run(&InMemoryStorage::new()).passed.into_iter().collect();
996 let uncovered: Vec<&String> = checks
997 .iter()
998 .filter(|check| !covered.contains(check.as_str()))
999 .collect();
1000 assert!(
1001 uncovered.is_empty(),
1002 "checks with no negative control: {uncovered:?}"
1003 );
1004
1005 for (fault, expected) in cases {
1006 let report = run(&Faulty::new(fault));
1007 assert!(!report.is_green(), "{fault:?} passed the suite");
1008 assert!(
1009 report.failures.iter().any(|f| f.check == expected),
1010 "{fault:?}: expected {expected} to fail, got: {}",
1011 report.summary()
1012 );
1013 }
1014 }
1015
1016 #[test]
1017 fn the_absent_key_check_names_the_wrong_error_variant() {
1018 let report = run(&Faulty::new(Fault::AbsentIsAnError));
1019 let failure = report
1020 .failures
1021 .iter()
1022 .find(|f| f.check == "absent_key_loads_as_none")
1023 .expect("the check failed");
1024 assert!(failure.detail.contains("KeyNotFound"), "{}", failure.detail);
1025 }
1026 #[test]
1027 fn a_backend_that_lists_by_suffix_stops_the_run_before_any_delete() {
1028 let storage = Faulty::new(Fault::ListsBySuffix);
1032 storage
1033 .store("prefixed_mls_storage_conformance_probe", "real", b"kept")
1034 .unwrap();
1035 let report = run(&storage);
1036 assert!(report.passed.is_empty(), "{:?}", report.passed);
1037 assert_eq!(
1038 report.failures[0].check,
1039 "key_types_are_separate_namespaces"
1040 );
1041 assert_eq!(
1042 storage
1043 .load("prefixed_mls_storage_conformance_probe", "real")
1044 .unwrap(),
1045 Some(b"kept".to_vec())
1046 );
1047 }
1048
1049 #[test]
1050 fn a_backend_that_merges_key_types_stops_the_run_before_any_delete() {
1051 let storage = Faulty::new(Fault::IgnoresKeyType);
1056 storage.store("identity", "key_pair", b"secret").unwrap();
1057 storage.store("group_state", "group:abc", b"state").unwrap();
1058 let report = run(&storage);
1059 assert!(report.passed.is_empty(), "{:?}", report.passed);
1060 assert_eq!(report.failures.len(), 1, "{}", report.summary());
1061 assert_eq!(
1062 report.failures[0].check,
1063 "key_types_are_separate_namespaces"
1064 );
1065 assert_eq!(
1066 storage.load("identity", "key_pair").unwrap(),
1067 Some(b"secret".to_vec())
1068 );
1069 assert_eq!(
1070 storage.load("group_state", "group:abc").unwrap(),
1071 Some(b"state".to_vec())
1072 );
1073 }
1074}