use alloc::borrow::Cow;
use alloc::vec;
use alloc::vec::Vec;
use super::util::{fisher_yates, zero_buffer};
use super::{FragmentStrategy, Fragments};
use crate::Result;
use crate::error::Error;
use crate::fetcher::RawKey;
use crate::memory::LockedBytes;
const DEFAULT_POOL_FACTOR: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct InterleavedFragmenter {
pool_factor: usize,
}
impl Default for InterleavedFragmenter {
fn default() -> Self {
Self::new()
}
}
impl InterleavedFragmenter {
#[must_use]
pub fn new() -> Self {
Self {
pool_factor: DEFAULT_POOL_FACTOR,
}
}
#[must_use]
pub fn with_pool_factor(factor: usize) -> Self {
Self {
pool_factor: factor.max(2),
}
}
}
impl FragmentStrategy for InterleavedFragmenter {
#[allow(clippy::cast_possible_truncation)]
fn fragment(&self, key: &RawKey) -> Result<Fragments> {
let bytes = key.as_bytes();
let key_len = bytes.len();
if key_len == 0 {
return Err(Error::Fragment(alloc::string::ToString::to_string(
"empty key cannot be fragmented",
)));
}
let pool_size = key_len.checked_mul(self.pool_factor).ok_or_else(|| {
Error::Fragment(alloc::string::ToString::to_string("pool size overflowed"))
})?;
if pool_size > u32::MAX as usize {
return Err(Error::Fragment(alloc::string::ToString::to_string(
"pool size exceeds u32 layout encoding",
)));
}
let mut all_positions: Vec<u32> = (0..pool_size as u32).collect();
fisher_yates(&mut all_positions)?;
all_positions.truncate(key_len);
let positions = all_positions;
let mut pool: Vec<u8> = vec![0u8; pool_size];
getrandom::getrandom(&mut pool).map_err(|_| Error::Internal("OS RNG failed"))?;
for (i, &pos) in positions.iter().enumerate() {
pool[pos as usize] = bytes[i];
}
let chunk = LockedBytes::from_slice(&pool);
zero_buffer(&mut pool);
drop(pool);
let mut layout_bytes: Vec<u8> = Vec::with_capacity(4 + key_len * 4);
layout_bytes.extend_from_slice(&(pool_size as u32).to_le_bytes());
for pos in &positions {
layout_bytes.extend_from_slice(&pos.to_le_bytes());
}
let layout = LockedBytes::from_slice(&layout_bytes);
zero_buffer(&mut layout_bytes);
drop(layout_bytes);
drop(positions);
Ok(Fragments::from_parts(alloc::vec![chunk], layout, key_len))
}
fn defragment(&self, fragments: &Fragments) -> Result<RawKey> {
let mut out = alloc::vec![0u8; fragments.total_len()];
self.defragment_into(fragments, &mut out)?;
Ok(RawKey::new(out))
}
fn defragment_into(&self, fragments: &Fragments, out: &mut [u8]) -> Result<()> {
let chunks = fragments.chunks();
if chunks.len() != 1 {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"interleaved fragments expects exactly one chunk",
)));
}
let pool = &chunks[0];
let layout = fragments.layout().as_bytes();
let key_len = fragments.total_len();
if out.len() != key_len {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"scratch buffer size does not match fragments.total_len()",
)));
}
if layout.len() != 4 + key_len * 4 {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"interleaved layout size does not match key length",
)));
}
let pool_size_raw: [u8; 4] = layout[0..4]
.try_into()
.map_err(|_| Error::Defragment(alloc::string::ToString::to_string("layout slice")))?;
let pool_size = u32::from_le_bytes(pool_size_raw) as usize;
if pool_size != pool.as_bytes().len() {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"interleaved layout pool size disagrees with chunk size",
)));
}
let pool_bytes = pool.as_bytes();
for (i, slot) in out.iter_mut().enumerate().take(key_len) {
let raw: [u8; 4] = layout[4 + i * 4..4 + (i + 1) * 4].try_into().map_err(|_| {
Error::Defragment(alloc::string::ToString::to_string("layout slice"))
})?;
let pos = u32::from_le_bytes(raw) as usize;
if pos >= pool_size {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"interleaved layout position out of range",
)));
}
*slot = pool_bytes[pos];
}
Ok(())
}
fn describe(&self) -> Cow<'_, str> {
Cow::Borrowed("interleaved")
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
mod tests {
use super::*;
fn key(bytes: &[u8]) -> RawKey {
RawKey::new(bytes.to_vec())
}
#[test]
fn round_trip_short_key() {
let frag = InterleavedFragmenter::new();
let original = key(&[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let fragments = frag.fragment(&original).unwrap();
let recovered = frag.defragment(&fragments).unwrap();
assert_eq!(recovered.as_bytes(), original.as_bytes());
}
#[test]
fn round_trip_many_sizes() {
let frag = InterleavedFragmenter::new();
for len in [1usize, 7, 16, 32, 64, 128, 256, 500, 1024] {
let bytes: Vec<u8> = (0..len).map(|i| ((i * 17) & 0xff) as u8).collect();
let original = key(&bytes);
let fragments = frag.fragment(&original).unwrap();
let recovered = frag.defragment(&fragments).unwrap();
assert_eq!(recovered.as_bytes(), &bytes[..], "mismatch at len {len}");
}
}
#[test]
fn empty_key_rejected() {
let frag = InterleavedFragmenter::new();
let err = frag.fragment(&key(&[])).unwrap_err();
assert!(matches!(err, Error::Fragment(_)));
}
#[test]
fn custom_pool_factor_changes_pool_size() {
let small = InterleavedFragmenter::with_pool_factor(2);
let large = InterleavedFragmenter::with_pool_factor(8);
let bytes: Vec<u8> = (0..16).collect();
let original = key(&bytes);
let s = small.fragment(&original).unwrap();
let l = large.fragment(&original).unwrap();
assert_eq!(s.chunks()[0].as_bytes().len(), 16 * 2);
assert_eq!(l.chunks()[0].as_bytes().len(), 16 * 8);
}
#[test]
fn pool_factor_one_is_clamped_to_two() {
let frag = InterleavedFragmenter::with_pool_factor(1);
let bytes: Vec<u8> = (0..16).collect();
let original = key(&bytes);
let fragments = frag.fragment(&original).unwrap();
assert_eq!(fragments.chunks()[0].as_bytes().len(), 16 * 2);
}
#[test]
fn describe_returns_interleaved() {
assert_eq!(InterleavedFragmenter::new().describe(), "interleaved");
}
}