use std::sync::Arc;
use selene_core::PropertyMap;
use selene_persist::MAX_SECTION_PAYLOAD_BYTES;
use crate::core_provider::{inconsistent, invalid_payload, serialization_failed};
pub(in crate::core_provider) fn ensure_section_within_cap(
section: &'static str,
len: usize,
) -> Result<(), crate::ProviderError> {
if len > MAX_SECTION_PAYLOAD_BYTES {
return Err(inconsistent(format!(
"{section} core section exceeds 1 GiB cap; multi-section split is a future v1.x hardening"
)));
}
Ok(())
}
pub(in crate::core_provider::sections) fn encode_rkyv<T>(
value: &T,
section: &'static str,
) -> Result<Vec<u8>, crate::ProviderError>
where
T: for<'a> rkyv::Serialize<
rkyv::api::high::HighSerializer<
rkyv::util::AlignedVec,
rkyv::ser::allocator::ArenaHandle<'a>,
rkyv::rancor::Error,
>,
>,
{
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(value)
.map_err(|error| serialization_failed(format!("{section} rkyv encode failed: {error}")))?
.into_vec();
ensure_section_within_cap(section, bytes.len())?;
Ok(bytes)
}
pub(in crate::core_provider::sections) fn decode_rkyv<T>(
bytes: &[u8],
section: &'static str,
) -> Result<T, crate::ProviderError>
where
T: rkyv::Archive,
T::Archived: for<'a> rkyv::bytecheck::CheckBytes<rkyv::api::high::HighValidator<'a, rkyv::rancor::Error>>
+ rkyv::Deserialize<T, rkyv::api::high::HighDeserializer<rkyv::rancor::Error>>,
{
ensure_section_within_cap(section, bytes.len())?;
rkyv::from_bytes::<T, rkyv::rancor::Error>(bytes).map_err(|error| {
invalid_payload(format!("{section} rkyv bytecheck/decode failed: {error}"))
})
}
pub(in crate::core_provider::sections) fn encode_properties_blob(
properties: &PropertyMap,
section: &'static str,
) -> Result<Arc<[u8]>, crate::ProviderError> {
let bytes = postcard::to_stdvec(properties).map_err(|error| {
serialization_failed(format!(
"{section} property postcard encode failed: {error}"
))
})?;
Ok(Arc::from(bytes.into_boxed_slice()))
}
pub(in crate::core_provider::sections) fn decode_properties_blob(
bytes: &[u8],
section: &'static str,
) -> Result<PropertyMap, crate::ProviderError> {
postcard::from_bytes(bytes).map_err(|error| {
invalid_payload(format!(
"{section} property postcard decode failed: {error}"
))
})
}
pub(in crate::core_provider::sections) fn validate_sorted_unique<K, V>(
rows: &[(K, V)],
section: &'static str,
) -> Result<(), crate::ProviderError>
where
K: Ord + std::fmt::Debug,
{
for pair in rows.windows(2) {
if pair[0].0 >= pair[1].0 {
return Err(invalid_payload(format!(
"{section} rows must be strictly sorted by key with no duplicates; observed {:?} then {:?}",
pair[0].0, pair[1].0
)));
}
}
Ok(())
}
pub(in crate::core_provider::sections) fn validate_ids_unique<K, V>(
rows: &[(K, V)],
tombstone: K,
section: &'static str,
) -> Result<(), crate::ProviderError>
where
K: Copy + Eq + std::hash::Hash + std::fmt::Debug,
{
let mut seen = std::collections::HashSet::new();
for (id, _) in rows {
if *id == tombstone {
continue;
}
if !seen.insert(*id) {
return Err(invalid_payload(format!(
"{section} rows must have unique non-tombstone ids; observed duplicate {id:?}"
)));
}
}
Ok(())
}
#[cfg(test)]
mod validate_ids_unique_tests {
use super::validate_ids_unique;
use selene_core::NodeId;
#[test]
fn rejects_duplicate_non_tombstone_id() {
let rows = [
(NodeId::new(5), ()),
(NodeId::TOMBSTONE, ()),
(NodeId::new(5), ()),
];
let err = validate_ids_unique(&rows, NodeId::TOMBSTONE, "CORE/NODE")
.expect_err("a duplicate committed id is a corrupt snapshot");
assert!(
format!("{err}").contains("unique non-tombstone"),
"unexpected error: {err}"
);
}
#[test]
fn allows_multiple_tombstone_hole_rows() {
let rows = [
(NodeId::new(1), ()),
(NodeId::TOMBSTONE, ()),
(NodeId::TOMBSTONE, ()),
(NodeId::new(2), ()),
];
validate_ids_unique(&rows, NodeId::TOMBSTONE, "CORE/NODE")
.expect("multiple tombstone hole rows are valid");
}
}