use crate::protocol_state_storage::{
ProtocolStateError, ProtocolStateStorage, MAX_PROTOCOL_STATE_RECORD_TRANSFER_BYTES,
};
pub const CONFORMANCE_KEY_TYPE: &str = "storage_conformance_probe";
pub const CONFORMANCE_KEY_TYPE_OTHER: &str = "storage_conformance_probe_other";
const CONFORMANCE_KEY_TYPE_SUFFIXED: &str = "suffixed_storage_conformance_probe";
const ISOLATION_PROBE_ID: &str = "storage_conformance_isolation";
const ISOLATION_LISTING_PROBE_ID: &str = "storage_conformance_isolation_listing";
pub use offline_protocol_mls::storage_conformance::{ConformanceFailure, ConformanceReport};
fn expect_load(
storage: &dyn ProtocolStateStorage,
key_id: &str,
expected: &[u8],
) -> std::result::Result<(), String> {
match storage.load(CONFORMANCE_KEY_TYPE, key_id) {
Ok(Some(actual)) if actual == expected => Ok(()),
Ok(Some(actual)) => Err(format!(
"loaded {} bytes, expected {} (contents differ)",
actual.len(),
expected.len()
)),
Ok(None) => Err("stored value loaded back as missing".to_string()),
Err(err) => Err(format!("load failed: {err}")),
}
}
pub fn run(storage: &dyn ProtocolStateStorage) -> ConformanceReport {
let mut report = ConformanceReport::new();
let isolation = key_types_are_isolated(storage);
let isolated = isolation.is_ok();
report.check("key_types_are_separate_namespaces", isolation);
if !isolated {
return report;
}
cleanup(storage);
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, "basic", b"hello")
.map_err(|err| format!("store failed: {err}"))
.and_then(|()| expect_load(storage, "basic", b"hello"));
report.check("store_then_load", outcome);
let binary: Vec<u8> = (0u8..=255).collect();
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, "binary", &binary)
.map_err(|err| format!("store failed: {err}"))
.and_then(|()| expect_load(storage, "binary", &binary));
report.check("binary_values_round_trip", outcome);
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, "empty", b"")
.map_err(|err| format!("store failed: {err}"))
.and_then(|()| expect_load(storage, "empty", b""));
report.check("empty_value_is_not_missing", outcome);
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, "overwrite", b"first")
.and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, "overwrite", b"second"))
.map_err(|err| format!("store failed: {err}"))
.and_then(|()| expect_load(storage, "overwrite", b"second"));
report.check("store_overwrites", outcome);
let outcome = match storage.load(CONFORMANCE_KEY_TYPE, "never_written") {
Ok(None) => Ok(()),
Ok(Some(_)) => Err("a key that was never written returned a value".to_string()),
Err(ProtocolStateError::NotFound(_)) => {
Err("absent keys must load as Ok(None), not NotFound".to_string())
}
Err(err) => Err(format!("load of an absent key failed: {err}")),
};
report.check("absent_key_loads_as_none", outcome);
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, "deletable", b"x")
.and_then(|()| storage.delete(CONFORMANCE_KEY_TYPE, "deletable"))
.map_err(|err| format!("store/delete failed: {err}"))
.and_then(|()| match storage.load(CONFORMANCE_KEY_TYPE, "deletable") {
Ok(None) => Ok(()),
Ok(Some(_)) => Err("value survived a delete".to_string()),
Err(err) => Err(format!("load after delete failed: {err}")),
})
.and_then(
|()| match storage.delete(CONFORMANCE_KEY_TYPE, "deletable") {
Ok(()) => Ok(()),
Err(err) => Err(format!("second delete must succeed, got: {err}")),
},
);
report.check("delete_removes_and_is_idempotent", outcome);
let outcome = (|| -> std::result::Result<(), String> {
storage
.store(CONFORMANCE_KEY_TYPE, "shared_id", b"mine")
.and_then(|()| storage.store(CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs"))
.map_err(|err| format!("store failed: {err}"))?;
let keys = storage
.list_keys(CONFORMANCE_KEY_TYPE)
.map_err(|err| format!("list_keys failed: {err}"))?;
for expected in ["basic", "binary", "empty", "overwrite", "shared_id"] {
if !keys.iter().any(|key| key == expected) {
return Err(format!("list_keys omitted {expected:?}"));
}
}
if keys.iter().any(|key| key == "deletable") {
return Err("list_keys named a deleted record".to_string());
}
let other = storage
.list_keys(CONFORMANCE_KEY_TYPE_OTHER)
.map_err(|err| format!("list_keys failed: {err}"))?;
if other.len() != 1 || other.first().map(String::as_str) != Some("shared_id") {
return Err(format!("list_keys leaked across key types: {other:?}"));
}
Ok(())
})();
report.check("list_keys_is_accurate_and_scoped", outcome);
let outcome = match storage.list_keys("storage_conformance_probe_unused") {
Ok(keys) if keys.is_empty() => Ok(()),
Ok(keys) => Err(format!("an unused key type listed {} keys", keys.len())),
Err(err) => Err(format!("list_keys on an unused key type failed: {err}")),
};
report.check("unused_key_type_lists_empty", outcome);
let composed = "space-1/doc.name_2/000000000000002a";
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, composed, b"composed")
.map_err(|err| format!("store with a composed key failed: {err}"))
.and_then(|()| expect_load(storage, composed, b"composed"))
.and_then(|()| {
let keys = storage
.list_keys(CONFORMANCE_KEY_TYPE)
.map_err(|err| format!("list_keys failed: {err}"))?;
if keys.iter().any(|key| key == composed) {
Ok(())
} else {
Err("list_keys did not return a composed key verbatim".to_string())
}
});
report.check("composed_key_ids_round_trip", outcome);
let long_key = format!("{}/{}/{:016x}", "s".repeat(128), "d".repeat(128), u64::MAX);
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, &long_key, b"long")
.map_err(|err| format!("store with a long key failed: {err}"))
.and_then(|()| expect_load(storage, &long_key, b"long"));
report.check("long_key_ids_round_trip", outcome);
let large = vec![0xa5u8; MAX_PROTOCOL_STATE_RECORD_TRANSFER_BYTES / 2];
let outcome = storage
.store(CONFORMANCE_KEY_TYPE, "large", &large)
.map_err(|err| format!("store of a large record failed: {err}"))
.and_then(|()| expect_load(storage, "large", &large));
report.check("large_records_round_trip", outcome);
let outcome = (|| -> std::result::Result<(), String> {
cleanup(storage);
for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
let remaining = storage
.list_keys(key_type)
.map_err(|err| format!("list_keys failed: {err}"))?;
if !remaining.is_empty() {
return Err(format!(
"{} records survived a listed delete in {key_type}",
remaining.len()
));
}
}
Ok(())
})();
report.check("listed_records_can_all_be_deleted", outcome);
report
}
fn key_types_are_isolated(storage: &dyn ProtocolStateStorage) -> std::result::Result<(), String> {
let outcome = (|| -> std::result::Result<(), String> {
storage
.store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID, b"theirs")
.and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID, b"mine"))
.and_then(|()| {
storage.store(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID, b"x")
})
.and_then(|()| {
storage.store(CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID, b"x")
})
.map_err(|err| {
format!("store failed, so isolation could not be checked and nothing was deleted: {err}")
})?;
if let Ok(Some(other)) = storage.load(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID) {
if other == b"mine" {
return Err(
"the same key id in two key types collided: a write under one \
replaced the other"
.to_string(),
);
}
}
if let Ok(listed) = storage.list_keys(CONFORMANCE_KEY_TYPE) {
if listed.iter().any(|key| key == ISOLATION_LISTING_PROBE_ID) {
return Err("list_keys named a record written under another key type".to_string());
}
}
Ok(())
})();
for (key_type, key_id) in [
(CONFORMANCE_KEY_TYPE, ISOLATION_PROBE_ID),
(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_PROBE_ID),
(CONFORMANCE_KEY_TYPE_OTHER, ISOLATION_LISTING_PROBE_ID),
(CONFORMANCE_KEY_TYPE_SUFFIXED, ISOLATION_LISTING_PROBE_ID),
] {
let _ = storage.delete(key_type, key_id);
}
outcome
}
fn cleanup(storage: &dyn ProtocolStateStorage) {
for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
if let Ok(keys) = storage.list_keys(key_type) {
for key in keys {
let _ = storage.delete(key_type, &key);
}
}
}
}
pub fn run_json(storage: &dyn ProtocolStateStorage) -> String {
let report = run(storage);
serde_json::to_string(&report).unwrap_or_else(|err| {
format!(
"{{\"passed\":[],\"failures\":[{{\"check\":\"report_serialization\",\"detail\":\"{}\"}}]}}",
err.to_string().replace('"', "'")
)
})
}