1use crate::protocol_state_storage::{
27 ProtocolStateError, ProtocolStateStorage, MAX_PROTOCOL_STATE_RECORD_TRANSFER_BYTES,
28};
29
30pub const CONFORMANCE_KEY_TYPE: &str = "storage_conformance_probe";
35
36pub const CONFORMANCE_KEY_TYPE_OTHER: &str = "storage_conformance_probe_other";
38
39const CONFORMANCE_KEY_TYPE_SUFFIXED: &str = "suffixed_storage_conformance_probe";
43
44const ISOLATION_PROBE_ID: &str = "storage_conformance_isolation";
48
49const ISOLATION_LISTING_PROBE_ID: &str = "storage_conformance_isolation_listing";
52
53pub use offline_protocol_mls::storage_conformance::{ConformanceFailure, ConformanceReport};
56
57fn expect_load(
58 storage: &dyn ProtocolStateStorage,
59 key_id: &str,
60 expected: &[u8],
61) -> std::result::Result<(), String> {
62 match storage.load(CONFORMANCE_KEY_TYPE, key_id) {
63 Ok(Some(actual)) if actual == expected => Ok(()),
64 Ok(Some(actual)) => Err(format!(
65 "loaded {} bytes, expected {} (contents differ)",
66 actual.len(),
67 expected.len()
68 )),
69 Ok(None) => Err("stored value loaded back as missing".to_string()),
70 Err(err) => Err(format!("load failed: {err}")),
71 }
72}
73
74pub fn run(storage: &dyn ProtocolStateStorage) -> ConformanceReport {
80 let mut report = ConformanceReport::new();
81
82 let isolation = key_types_are_isolated(storage);
89 let isolated = isolation.is_ok();
90 report.check("key_types_are_separate_namespaces", isolation);
91 if !isolated {
92 return report;
93 }
94
95 cleanup(storage);
98
99 let outcome = storage
101 .store(CONFORMANCE_KEY_TYPE, "basic", b"hello")
102 .map_err(|err| format!("store failed: {err}"))
103 .and_then(|()| expect_load(storage, "basic", b"hello"));
104 report.check("store_then_load", outcome);
105
106 let binary: Vec<u8> = (0u8..=255).collect();
110 let outcome = storage
111 .store(CONFORMANCE_KEY_TYPE, "binary", &binary)
112 .map_err(|err| format!("store failed: {err}"))
113 .and_then(|()| expect_load(storage, "binary", &binary));
114 report.check("binary_values_round_trip", outcome);
115
116 let outcome = storage
118 .store(CONFORMANCE_KEY_TYPE, "empty", b"")
119 .map_err(|err| format!("store failed: {err}"))
120 .and_then(|()| expect_load(storage, "empty", b""));
121 report.check("empty_value_is_not_missing", outcome);
122
123 let outcome = storage
125 .store(CONFORMANCE_KEY_TYPE, "overwrite", b"first")
126 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, "overwrite", b"second"))
127 .map_err(|err| format!("store failed: {err}"))
128 .and_then(|()| expect_load(storage, "overwrite", b"second"));
129 report.check("store_overwrites", outcome);
130
131 let outcome = match storage.load(CONFORMANCE_KEY_TYPE, "never_written") {
133 Ok(None) => Ok(()),
134 Ok(Some(_)) => Err("a key that was never written returned a value".to_string()),
135 Err(ProtocolStateError::NotFound(_)) => {
136 Err("absent keys must load as Ok(None), not NotFound".to_string())
137 }
138 Err(err) => Err(format!("load of an absent key failed: {err}")),
139 };
140 report.check("absent_key_loads_as_none", outcome);
141
142 let outcome = storage
147 .store(CONFORMANCE_KEY_TYPE, "deletable", b"x")
148 .and_then(|()| storage.delete(CONFORMANCE_KEY_TYPE, "deletable"))
149 .map_err(|err| format!("store/delete failed: {err}"))
150 .and_then(|()| match storage.load(CONFORMANCE_KEY_TYPE, "deletable") {
151 Ok(None) => Ok(()),
152 Ok(Some(_)) => Err("value survived a delete".to_string()),
153 Err(err) => Err(format!("load after delete failed: {err}")),
154 })
155 .and_then(
156 |()| match storage.delete(CONFORMANCE_KEY_TYPE, "deletable") {
157 Ok(()) => Ok(()),
158 Err(err) => Err(format!("second delete must succeed, got: {err}")),
159 },
160 );
161 report.check("delete_removes_and_is_idempotent", outcome);
162
163 let outcome = (|| -> std::result::Result<(), String> {
165 storage
166 .store(CONFORMANCE_KEY_TYPE, "shared_id", b"mine")
167 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs"))
168 .map_err(|err| format!("store failed: {err}"))?;
169 let keys = storage
170 .list_keys(CONFORMANCE_KEY_TYPE)
171 .map_err(|err| format!("list_keys failed: {err}"))?;
172 for expected in ["basic", "binary", "empty", "overwrite", "shared_id"] {
173 if !keys.iter().any(|key| key == expected) {
174 return Err(format!("list_keys omitted {expected:?}"));
175 }
176 }
177 if keys.iter().any(|key| key == "deletable") {
178 return Err("list_keys named a deleted record".to_string());
179 }
180 let other = storage
181 .list_keys(CONFORMANCE_KEY_TYPE_OTHER)
182 .map_err(|err| format!("list_keys failed: {err}"))?;
183 if other.len() != 1 || other.first().map(String::as_str) != Some("shared_id") {
184 return Err(format!("list_keys leaked across key types: {other:?}"));
185 }
186 Ok(())
187 })();
188 report.check("list_keys_is_accurate_and_scoped", outcome);
189
190 let outcome = match storage.list_keys("storage_conformance_probe_unused") {
192 Ok(keys) if keys.is_empty() => Ok(()),
193 Ok(keys) => Err(format!("an unused key type listed {} keys", keys.len())),
194 Err(err) => Err(format!("list_keys on an unused key type failed: {err}")),
195 };
196 report.check("unused_key_type_lists_empty", outcome);
197
198 let composed = "space-1/doc.name_2/000000000000002a";
203 let outcome = storage
204 .store(CONFORMANCE_KEY_TYPE, composed, b"composed")
205 .map_err(|err| format!("store with a composed key failed: {err}"))
206 .and_then(|()| expect_load(storage, composed, b"composed"))
207 .and_then(|()| {
208 let keys = storage
209 .list_keys(CONFORMANCE_KEY_TYPE)
210 .map_err(|err| format!("list_keys failed: {err}"))?;
211 if keys.iter().any(|key| key == composed) {
212 Ok(())
213 } else {
214 Err("list_keys did not return a composed key verbatim".to_string())
215 }
216 });
217 report.check("composed_key_ids_round_trip", outcome);
218
219 let long_key = format!("{}/{}/{:016x}", "s".repeat(128), "d".repeat(128), u64::MAX);
227 let outcome = storage
228 .store(CONFORMANCE_KEY_TYPE, &long_key, b"long")
229 .map_err(|err| format!("store with a long key failed: {err}"))
230 .and_then(|()| expect_load(storage, &long_key, b"long"));
231 report.check("long_key_ids_round_trip", outcome);
232
233 let large = vec![0xa5u8; MAX_PROTOCOL_STATE_RECORD_TRANSFER_BYTES / 2];
237 let outcome = storage
238 .store(CONFORMANCE_KEY_TYPE, "large", &large)
239 .map_err(|err| format!("store of a large record failed: {err}"))
240 .and_then(|()| expect_load(storage, "large", &large));
241 report.check("large_records_round_trip", outcome);
242
243 let outcome = (|| -> std::result::Result<(), String> {
247 cleanup(storage);
248 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
249 let remaining = storage
250 .list_keys(key_type)
251 .map_err(|err| format!("list_keys failed: {err}"))?;
252 if !remaining.is_empty() {
253 return Err(format!(
254 "{} records survived a listed delete in {key_type}",
255 remaining.len()
256 ));
257 }
258 }
259 Ok(())
260 })();
261 report.check("listed_records_can_all_be_deleted", outcome);
262
263 report
264}
265
266fn key_types_are_isolated(storage: &dyn ProtocolStateStorage) -> std::result::Result<(), String> {
275 let outcome = (|| -> std::result::Result<(), String> {
276 storage
277 .store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID, b"theirs")
278 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID, b"mine"))
279 .and_then(|()| {
280 storage.store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID, b"x")
281 })
282 .and_then(|()| {
283 storage.store(CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID, b"x")
284 })
285 .map_err(|err| {
286 format!("store failed, so isolation could not be checked and nothing was deleted: {err}")
287 })?;
288 if let Ok(Some(other)) = storage.load(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID) {
289 if other == b"mine" {
290 return Err(
291 "the same key id in two key types collided: a write under one \
292 replaced the other"
293 .to_string(),
294 );
295 }
296 }
297 if let Ok(listed) = storage.list_keys(CONFORMANCE_KEY_TYPE) {
298 if listed.iter().any(|key| key == ISOLATION_LISTING_PROBE_ID) {
299 return Err("list_keys named a record written under another key type".to_string());
300 }
301 }
302 Ok(())
303 })();
304 for (key_type, key_id) in [
305 (CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID),
306 (CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID),
307 (CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID),
308 (CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID),
309 ] {
310 let _ = storage.delete(key_type, key_id);
311 }
312 outcome
313}
314
315fn cleanup(storage: &dyn ProtocolStateStorage) {
317 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
318 if let Ok(keys) = storage.list_keys(key_type) {
319 for key in keys {
320 let _ = storage.delete(key_type, &key);
321 }
322 }
323 }
324}
325
326pub fn run_json(storage: &dyn ProtocolStateStorage) -> String {
331 let report = run(storage);
332 serde_json::to_string(&report).unwrap_or_else(|err| {
333 format!(
334 "{{\"passed\":[],\"failures\":[{{\"check\":\"report_serialization\",\"detail\":\"{}\"}}]}}",
335 err.to_string().replace('"', "'")
336 )
337 })
338}