use super::{
bflatn_from::read_tag,
indexes::{Bytes, PageOffset},
page::Page,
var_len::VarLenRef,
};
use crate::{bflatn_from::vlr_blob_bytes, blob_store::BlobStore};
use core::hash::{Hash as _, Hasher};
use core::mem;
use core::str;
use spacetimedb_sats::layout::{
align_to, AlgebraicTypeLayout, HasLayout, ProductTypeLayoutView, RowTypeLayout, VarLenType,
};
use spacetimedb_sats::{algebraic_value::ser::concat_byte_chunks_buf, bsatn::Deserializer, i256, u256, F32, F64};
pub unsafe fn hash_row_in_page(
hasher: &mut impl Hasher,
page: &Page,
blob_store: &dyn BlobStore,
fixed_offset: PageOffset,
ty: &RowTypeLayout,
) {
let fixed_bytes = page.get_row_data(fixed_offset, ty.size());
unsafe { hash_product(hasher, fixed_bytes, page, blob_store, &mut 0, ty.product()) };
}
unsafe fn hash_product(
hasher: &mut impl Hasher,
bytes: &Bytes,
page: &Page,
blob_store: &dyn BlobStore,
curr_offset: &mut usize,
ty: ProductTypeLayoutView<'_>,
) {
let base_offset = *curr_offset;
for elem_ty in ty.elements {
*curr_offset = base_offset + elem_ty.offset as usize;
unsafe {
hash_value(hasher, bytes, page, blob_store, curr_offset, &elem_ty.ty);
}
}
}
unsafe fn hash_value(
hasher: &mut impl Hasher,
bytes: &Bytes,
page: &Page,
blob_store: &dyn BlobStore,
curr_offset: &mut usize,
ty: &AlgebraicTypeLayout,
) {
debug_assert_eq!(
*curr_offset,
align_to(*curr_offset, ty.align()),
"curr_offset {} insufficiently aligned for type {:?}",
*curr_offset,
ty
);
match ty {
AlgebraicTypeLayout::Sum(ty) => {
let (tag, data_ty) = read_tag(bytes, ty, *curr_offset);
tag.hash(hasher);
let mut data_offset = *curr_offset + ty.offset_of_variant_data(tag);
unsafe { hash_value(hasher, bytes, page, blob_store, &mut data_offset, data_ty) };
*curr_offset += ty.size();
}
AlgebraicTypeLayout::Product(ty) => {
unsafe { hash_product(hasher, bytes, page, blob_store, curr_offset, ty.view()) }
}
&AlgebraicTypeLayout::Bool | &AlgebraicTypeLayout::U8 => {
u8::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher)
}
&AlgebraicTypeLayout::I8 => i8::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::I16 => i16::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::U16 => u16::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::I32 => i32::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::U32 => u32::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::I64 => i64::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::U64 => u64::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::I128 => i128::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::U128 => u128::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::I256 => i256::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::U256 => u256::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) }).hash(hasher),
&AlgebraicTypeLayout::F32 => {
F32::from(f32::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) })).hash(hasher)
}
&AlgebraicTypeLayout::F64 => {
F64::from(f64::from_le_bytes(unsafe { read_from_bytes(bytes, curr_offset) })).hash(hasher)
}
&AlgebraicTypeLayout::String => {
unsafe {
run_vlo_bytes(page, bytes, blob_store, curr_offset, |bytes| {
let string = str::from_utf8_unchecked(bytes);
string.hash(hasher)
});
}
}
AlgebraicTypeLayout::VarLen(VarLenType::Array(ty)) => {
let ty = &ty.elem_ty;
unsafe {
run_vlo_bytes(page, bytes, blob_store, curr_offset, |mut bsatn| {
let de = Deserializer::new(&mut bsatn);
spacetimedb_sats::hash_bsatn_array(hasher, ty, de).unwrap();
});
}
}
}
}
pub(crate) unsafe fn run_vlo_bytes<R>(
page: &Page,
bytes: &Bytes,
blob_store: &dyn BlobStore,
curr_offset: &mut usize,
run: impl FnOnce(&[u8]) -> R,
) -> R {
let vlr = unsafe { read_from_bytes::<VarLenRef>(bytes, curr_offset) };
if vlr.is_large_blob() {
let bytes = unsafe { vlr_blob_bytes(page, blob_store, vlr) };
run(bytes)
} else {
let var_iter = unsafe { page.iter_vlo_data(vlr.first_granule) };
let total_len = vlr.length_in_bytes as usize;
unsafe { concat_byte_chunks_buf(total_len, var_iter, run) }
}
}
pub unsafe fn read_from_bytes<T: Copy>(bytes: &Bytes, curr_offset: &mut usize) -> T {
let bytes = &bytes[*curr_offset..];
*curr_offset += mem::size_of::<T>();
let ptr: *const T = bytes.as_ptr().cast();
unsafe { *ptr }
}
#[cfg(test)]
mod tests {
use crate::{blob_store::HashMapBlobStore, page_pool::PagePool};
use core::hash::BuildHasher;
use proptest::prelude::*;
use spacetimedb_sats::proptest::generate_typed_row;
proptest! {
#![proptest_config(ProptestConfig::with_cases(if cfg!(miri) { 8 } else { 2048 }))]
#[test]
fn pv_row_ref_hash_same_std_random_state((ty, val) in generate_typed_row()) {
let mut table = crate::table::test::table(ty);
let pool = &PagePool::new_for_test();
let blob_store = &mut HashMapBlobStore::default();
let (_, row) = table.insert(pool, blob_store, &val).unwrap();
let rs = std::hash::RandomState::new();
prop_assert_eq!(rs.hash_one(&val), rs.hash_one(row));
}
#[test]
fn pv_row_ref_hash_same_ahash((ty, val) in generate_typed_row()) {
let pool = &PagePool::new_for_test();
let blob_store = &mut HashMapBlobStore::default();
let mut table = crate::table::test::table(ty);
let (_, row) = table.insert(pool, blob_store, &val).unwrap();
let rs = std::hash::RandomState::new();
prop_assert_eq!(rs.hash_one(&val), rs.hash_one(row));
}
}
}