use crate::{
db::{
codec::{
finalize_hash_sha256, new_hash_sha256_prefixed, write_hash_len_u32, write_hash_str_u32,
write_hash_tag_u8, write_hash_u64,
},
integrity::DatabaseIncarnationId,
schema::{AcceptedSchemaRevision, enum_catalog::AcceptedSchemaRoot},
},
error::InternalError,
};
use sha2::Digest;
const ACCEPTED_RUNTIME_ROOT_FINGERPRINT_DOMAIN: &[u8] = b"icydb.accepted-runtime-root.v1";
const ACCEPTED_RUNTIME_ROOT_FINGERPRINT_METHOD_VERSION: u8 = 1;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct AcceptedSchemaRuntimeStoreRoot {
store_path: &'static str,
root: Option<AcceptedSchemaRoot>,
}
impl AcceptedSchemaRuntimeStoreRoot {
#[must_use]
pub(in crate::db) const fn new(
store_path: &'static str,
root: Option<AcceptedSchemaRoot>,
) -> Self {
Self { store_path, root }
}
#[must_use]
pub(in crate::db) const fn store_path(self) -> &'static str {
self.store_path
}
#[must_use]
pub(in crate::db) const fn root(self) -> Option<AcceptedSchemaRoot> {
self.root
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(in crate::db) struct AcceptedSchemaRuntimeRootIdentity {
database_incarnation: DatabaseIncarnationId,
accepted_root_revision: AcceptedSchemaRevision,
fingerprint_method_version: u8,
fingerprint: [u8; 32],
}
impl AcceptedSchemaRuntimeRootIdentity {
pub(in crate::db) fn from_store_roots(
database_incarnation: DatabaseIncarnationId,
store_roots: &[AcceptedSchemaRuntimeStoreRoot],
) -> Result<Self, InternalError> {
if store_roots
.windows(2)
.any(|pair| pair[0].store_path() >= pair[1].store_path())
{
return Err(InternalError::store_invariant());
}
let accepted_root_revision = store_roots
.iter()
.filter_map(|store| store.root().map(AcceptedSchemaRoot::revision))
.max()
.unwrap_or(AcceptedSchemaRevision::NONE);
let mut hasher = new_hash_sha256_prefixed(ACCEPTED_RUNTIME_ROOT_FINGERPRINT_DOMAIN);
write_hash_len_u32(&mut hasher, store_roots.len());
for store in store_roots {
write_hash_str_u32(&mut hasher, store.store_path());
match store.root() {
None => write_hash_tag_u8(&mut hasher, 0),
Some(root) => {
write_hash_tag_u8(&mut hasher, 1);
write_hash_u64(&mut hasher, root.revision().get());
hasher.update(root.fingerprint().as_bytes());
}
}
}
Ok(Self {
database_incarnation,
accepted_root_revision,
fingerprint_method_version: ACCEPTED_RUNTIME_ROOT_FINGERPRINT_METHOD_VERSION,
fingerprint: finalize_hash_sha256(hasher),
})
}
#[must_use]
pub(in crate::db) const fn database_incarnation(self) -> DatabaseIncarnationId {
self.database_incarnation
}
#[must_use]
pub(in crate::db) const fn accepted_root_revision(self) -> AcceptedSchemaRevision {
self.accepted_root_revision
}
#[must_use]
pub(in crate::db) const fn fingerprint(self) -> (u8, [u8; 32]) {
(self.fingerprint_method_version, self.fingerprint)
}
}
#[cfg(test)]
mod tests {
use super::{AcceptedSchemaRuntimeRootIdentity, AcceptedSchemaRuntimeStoreRoot};
use crate::db::{
integrity::DatabaseIncarnationId,
schema::{AcceptedSchemaRevision, empty_accepted_schema_candidate_for_tests},
};
fn store_root(
store_path: &'static str,
revision: AcceptedSchemaRevision,
) -> AcceptedSchemaRuntimeStoreRoot {
let candidate = empty_accepted_schema_candidate_for_tests(store_path, revision);
AcceptedSchemaRuntimeStoreRoot::new(store_path, Some(candidate.root()))
}
#[test]
fn runtime_root_identity_binds_every_store_root_and_database_incarnation() {
let first = AcceptedSchemaRuntimeRootIdentity::from_store_roots(
DatabaseIncarnationId::for_tests(1),
&[
store_root("test::First", AcceptedSchemaRevision::INITIAL),
store_root("test::Second", AcceptedSchemaRevision::INITIAL),
],
)
.expect("canonical accepted roots should admit");
let changed_store = AcceptedSchemaRuntimeRootIdentity::from_store_roots(
DatabaseIncarnationId::for_tests(1),
&[
store_root("test::First", AcceptedSchemaRevision::INITIAL),
store_root("test::Second", AcceptedSchemaRevision::new(2)),
],
)
.expect("changed accepted root should admit");
let changed_incarnation = AcceptedSchemaRuntimeRootIdentity::from_store_roots(
DatabaseIncarnationId::for_tests(2),
&[
store_root("test::First", AcceptedSchemaRevision::INITIAL),
store_root("test::Second", AcceptedSchemaRevision::INITIAL),
],
)
.expect("changed database incarnation should admit");
assert_ne!(first, changed_store);
assert_ne!(first, changed_incarnation);
}
#[test]
fn runtime_root_identity_rejects_noncanonical_store_order() {
let stores = [
AcceptedSchemaRuntimeStoreRoot::new("test::Second", None),
AcceptedSchemaRuntimeStoreRoot::new("test::First", None),
];
assert!(
AcceptedSchemaRuntimeRootIdentity::from_store_roots(
DatabaseIncarnationId::for_tests(1),
&stores,
)
.is_err(),
);
}
}