use alloc::borrow::Cow;
use alloc::vec::Vec;
use super::util::{fisher_yates, sample_range, zero_buffer};
use super::{FragmentStrategy, Fragments};
use crate::Result;
use crate::error::Error;
use crate::fetcher::RawKey;
use crate::memory::LockedBytes;
const DEFAULT_MIN_CHUNK: usize = 1;
const DEFAULT_MAX_CHUNK: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct RandomFragmenter {
min_chunk: usize,
max_chunk: usize,
}
impl Default for RandomFragmenter {
fn default() -> Self {
Self::new()
}
}
impl RandomFragmenter {
#[must_use]
pub fn new() -> Self {
Self {
min_chunk: DEFAULT_MIN_CHUNK,
max_chunk: DEFAULT_MAX_CHUNK,
}
}
#[must_use]
pub fn with_chunk_range(min: usize, max: usize) -> Self {
let min = min.max(1);
let max = max.max(min);
Self {
min_chunk: min,
max_chunk: max,
}
}
}
impl FragmentStrategy for RandomFragmenter {
#[allow(clippy::cast_possible_truncation)]
fn fragment(&self, key: &RawKey) -> Result<Fragments> {
let bytes = key.as_bytes();
let total_len = bytes.len();
if total_len == 0 {
return Err(Error::Fragment(alloc::string::ToString::to_string(
"empty key cannot be fragmented",
)));
}
if total_len > u32::MAX as usize {
return Err(Error::Fragment(alloc::string::ToString::to_string(
"key too large for fragmentation",
)));
}
let mut positions: Vec<u32> = (0..total_len as u32).collect();
fisher_yates(&mut positions)?;
let mut chunks: Vec<LockedBytes> = Vec::new();
let mut layout_bytes: Vec<u8> = Vec::new();
let mut cursor = 0usize;
while cursor < positions.len() {
let remaining = positions.len() - cursor;
let size = if remaining <= self.max_chunk {
remaining
} else {
let pick = sample_range(self.min_chunk, self.max_chunk)?;
pick.min(remaining.saturating_sub(self.min_chunk))
.max(self.min_chunk)
.min(self.max_chunk)
.min(remaining)
};
let mut chunk_bytes: Vec<u8> = Vec::with_capacity(size);
for &pos in &positions[cursor..cursor + size] {
chunk_bytes.push(bytes[pos as usize]);
}
chunks.push(LockedBytes::from_slice(&chunk_bytes));
zero_buffer(&mut chunk_bytes);
drop(chunk_bytes);
layout_bytes.extend_from_slice(&(size as u32).to_le_bytes());
for &pos in &positions[cursor..cursor + size] {
layout_bytes.extend_from_slice(&pos.to_le_bytes());
}
cursor += size;
}
let layout = LockedBytes::from_slice(&layout_bytes);
zero_buffer(&mut layout_bytes);
drop(layout_bytes);
drop(positions);
Ok(Fragments::from_parts(chunks, layout, total_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 layout = fragments.layout().as_bytes();
let chunks = fragments.chunks();
let total_len = fragments.total_len();
if out.len() != total_len {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"scratch buffer size does not match fragments.total_len()",
)));
}
let mut layout_cursor = 0usize;
for chunk in chunks {
if layout_cursor + 4 > layout.len() {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"layout buffer truncated before size prefix",
)));
}
let size_raw: [u8; 4] = layout[layout_cursor..layout_cursor + 4]
.try_into()
.map_err(|_| {
Error::Defragment(alloc::string::ToString::to_string("layout slice"))
})?;
let size = u32::from_le_bytes(size_raw) as usize;
layout_cursor += 4;
if size != chunk.as_bytes().len() {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"layout size does not match chunk length",
)));
}
if layout_cursor + size * 4 > layout.len() {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"layout buffer truncated before position list",
)));
}
for (i, byte) in chunk.as_bytes().iter().enumerate() {
let pos_raw: [u8; 4] = layout[layout_cursor + i * 4..layout_cursor + (i + 1) * 4]
.try_into()
.map_err(|_| {
Error::Defragment(alloc::string::ToString::to_string("layout slice"))
})?;
let pos = u32::from_le_bytes(pos_raw) as usize;
if pos >= total_len {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"layout position out of range",
)));
}
out[pos] = *byte;
}
layout_cursor += size * 4;
}
if layout_cursor != layout.len() {
return Err(Error::Defragment(alloc::string::ToString::to_string(
"trailing bytes in layout buffer",
)));
}
Ok(())
}
fn describe(&self) -> Cow<'_, str> {
Cow::Borrowed("random")
}
}
#[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 = RandomFragmenter::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 = RandomFragmenter::new();
for len in [1usize, 7, 16, 32, 64, 128, 255, 256, 500, 1024] {
let bytes: Vec<u8> = (0..len).map(|i| (i & 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 = RandomFragmenter::new();
let err = frag.fragment(&key(&[])).unwrap_err();
assert!(matches!(err, Error::Fragment(_)));
}
#[test]
fn two_calls_produce_different_layouts() {
let frag = RandomFragmenter::new();
let bytes: Vec<u8> = (0..32).map(|i| i as u8).collect();
let original = key(&bytes);
let a = frag.fragment(&original).unwrap();
let b = frag.fragment(&original).unwrap();
assert_ne!(a.layout().as_bytes(), b.layout().as_bytes());
}
#[test]
fn describe_returns_random() {
assert_eq!(RandomFragmenter::new().describe(), "random");
}
}