use std::future::Future;
use std::time::Duration;
use futures::StreamExt;
use rand010::seq::SliceRandom;
use rand010::RngExt as _;
use crate::delay::Delay;
use crate::rng::{sample_bounded, validate_sampling, CryptoRng, RngSource, Sampling};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Snap {
Spacing(u64),
Checkpoints(Vec<u64>),
}
impl Snap {
fn down(&self, x: u64) -> Option<u64> {
match self {
Snap::Spacing(spacing) => Some(x - x % spacing),
Snap::Checkpoints(cps) => match cps.partition_point(|&cp| cp <= x) {
0 => None,
n => Some(cps[n - 1]),
},
}
}
fn up(&self, x: u64) -> Option<u64> {
match self {
Snap::Spacing(spacing) => x.div_ceil(*spacing).checked_mul(*spacing),
Snap::Checkpoints(cps) => cps.get(cps.partition_point(|&cp| cp < x)).copied(),
}
}
}
#[derive(Debug, Clone)]
struct Jitter {
sampling: Sampling,
max: u64,
}
#[derive(Debug)]
pub struct Range {
start: u64,
end: u64,
chunk_size: Option<(u64, u64)>,
overlap: Option<(u64, u64)>,
jitter: Option<Jitter>,
floor: u64,
snap: Option<Snap>,
rng: RngSource,
}
impl Range {
pub fn new(start: u64, end: u64) -> Self {
assert!(start < end, "empty or inverted range: {start}..{end}");
Self {
start,
end,
chunk_size: None,
overlap: None,
jitter: None,
floor: 0,
snap: None,
rng: RngSource::default(),
}
}
pub fn chunk_size(mut self, bounds: std::ops::RangeInclusive<u64>) -> Self {
let (min, max) = bounds.into_inner();
assert!(min >= 1, "chunk size must be at least 1");
assert!(min <= max, "chunk size bounds inverted: {min} > {max}");
self.chunk_size = Some((min, max));
self
}
pub fn overlap(mut self, bounds: std::ops::RangeInclusive<u64>) -> Self {
let (min, max) = bounds.into_inner();
assert!(min <= max, "overlap bounds inverted: {min} > {max}");
self.overlap = Some((min, max));
self
}
pub fn start_jitter(mut self, max_jitter: u64) -> Self {
self.jitter = Some(Jitter {
sampling: Sampling::Uniform,
max: max_jitter,
});
self
}
pub fn start_jitter_sampled(mut self, sampling: Sampling, max_jitter: u64) -> Self {
validate_sampling(&sampling);
self.jitter = Some(Jitter {
sampling,
max: max_jitter,
});
self
}
pub fn start_jitter_default(mut self) -> Self {
let total = self.end - self.start;
self.jitter = Some(Jitter {
sampling: Sampling::Uniform,
max: (total / 10).max(1),
});
self
}
pub fn floor(mut self, floor: u64) -> Self {
self.floor = floor;
self
}
pub fn snap_start(mut self, mut snap: Snap) -> Self {
match &mut snap {
Snap::Spacing(spacing) => assert!(*spacing > 0, "snap spacing must be positive"),
Snap::Checkpoints(cps) => {
assert!(!cps.is_empty(), "checkpoint list must not be empty");
cps.sort_unstable();
}
}
self.snap = Some(snap);
self
}
pub fn seed(mut self, seed: [u8; 32]) -> Self {
self.rng = RngSource::seeded(seed);
self
}
pub fn with_rng<R: CryptoRng + Send + 'static>(mut self, rng: R) -> Self {
self.rng = RngSource::custom(rng);
self
}
pub fn plan(mut self) -> ChunkPlan {
let total = self.end - self.start;
let (chunk_min, chunk_max) = self
.chunk_size
.unwrap_or(((total / 50).max(1), (total / 10).max(1)));
let (overlap_min, overlap_max) = self
.overlap
.unwrap_or((1, (total / 20).clamp(1, (chunk_max / 2).max(1))));
let jitter_amount = self
.jitter
.as_ref()
.map(|j| sample_bounded(&mut self.rng, &j.sampling, 0.0, j.max as f64).round() as u64);
let start = obfuscated_start(self.start, self.floor, jitter_amount, self.snap.as_ref());
let mut chunks = Vec::new();
let mut cursor = start;
loop {
let size = self.rng.random_range(chunk_min..=chunk_max);
let chunk_end = cursor.saturating_add(size).min(self.end);
chunks.push((cursor, chunk_end));
if chunk_end == self.end {
break;
}
let size = chunk_end - cursor;
let max_valid = (size - 1).min(chunk_end - start);
let hi = overlap_max.min(max_valid);
let lo = overlap_min.min(hi);
let overlap = self.rng.random_range(lo..=hi);
cursor = chunk_end - overlap;
}
let mut order: Vec<usize> = (0..chunks.len()).collect();
order.shuffle(&mut self.rng);
ChunkPlan {
chunks,
order,
position: 0,
delay: None,
}
}
}
fn obfuscated_start(true_start: u64, floor: u64, jitter: Option<u64>, snap: Option<&Snap>) -> u64 {
let floor = floor.min(true_start);
let jittered = true_start.saturating_sub(jitter.unwrap_or(0)).max(floor);
let Some(snap) = snap else { return jittered };
match snap.down(jittered) {
Some(snapped) if snapped >= floor => snapped,
_ => snap
.up(floor)
.filter(|&up| up <= true_start)
.unwrap_or(jittered),
}
}
#[derive(Debug)]
pub struct ChunkPlan {
chunks: Vec<(u64, u64)>,
order: Vec<usize>,
position: usize,
delay: Option<Delay>,
}
impl ChunkPlan {
pub fn len(&self) -> usize {
self.chunks.len()
}
pub fn is_empty(&self) -> bool {
self.chunks.is_empty()
}
pub fn chunks(&self) -> &[(u64, u64)] {
&self.chunks
}
pub fn start(&self) -> u64 {
self.chunks.first().map(|c| c.0).unwrap_or(0)
}
pub fn end(&self) -> u64 {
self.chunks.last().map(|c| c.1).unwrap_or(0)
}
pub fn delay(mut self, delay: Delay) -> Self {
self.delay = Some(delay);
self
}
pub async fn for_each<F, Fut>(mut self, mut f: F)
where
F: FnMut(u64, u64) -> Fut,
Fut: Future<Output = ()>,
{
let mut delay = self.delay.take();
for (start, end) in &mut self {
match delay.as_mut() {
Some(delay) => delay.run(f(start, end)).await,
None => f(start, end).await,
}
}
}
pub async fn for_each_concurrent<F, Fut>(mut self, limit: usize, f: F)
where
F: Fn(u64, u64) -> Fut,
Fut: Future<Output = ()>,
{
let mut delay = self.delay.take();
let jobs: Vec<((u64, u64), Option<Duration>)> = (&mut self)
.map(|chunk| (chunk, delay.as_mut().map(|d| d.sample())))
.collect();
futures::stream::iter(jobs)
.for_each_concurrent(limit.max(1), |((start, end), delay)| {
let f = &f;
async move {
if let Some(delay) = delay {
crate::timer::sleep(delay).await;
}
f(start, end).await;
}
})
.await;
}
pub fn stream_concurrent<F, Fut, T>(
mut self,
limit: usize,
f: F,
) -> impl futures::Stream<Item = T>
where
F: Fn(u64, u64) -> Fut,
Fut: Future<Output = T>,
{
let mut delay = self.delay.take();
let jobs: Vec<((u64, u64), Option<Duration>)> = (&mut self)
.map(|chunk| (chunk, delay.as_mut().map(|d| d.sample())))
.collect();
futures::stream::iter(jobs)
.map(move |((start, end), delay)| {
let work = f(start, end);
async move {
if let Some(delay) = delay {
crate::timer::sleep(delay).await;
}
work.await
}
})
.buffer_unordered(limit.max(1))
}
}
impl Iterator for ChunkPlan {
type Item = (u64, u64);
fn next(&mut self) -> Option<Self::Item> {
let idx = *self.order.get(self.position)?;
self.position += 1;
Some(self.chunks[idx])
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.order.len() - self.position;
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for ChunkPlan {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snapping_rounds_down_to_spacing() {
assert_eq!(
obfuscated_start(1042, 0, None, Some(&Snap::Spacing(100))),
1000
);
assert_eq!(
obfuscated_start(1097, 0, None, Some(&Snap::Spacing(100))),
1000
);
assert_eq!(
obfuscated_start(1100, 0, None, Some(&Snap::Spacing(100))),
1100
);
}
#[test]
fn snapping_uses_greatest_checkpoint_not_exceeding_start() {
let cps = Snap::Checkpoints(vec![500, 1000, 1500]);
assert_eq!(obfuscated_start(1042, 0, None, Some(&cps)), 1000);
assert_eq!(obfuscated_start(1600, 0, None, Some(&cps)), 1500);
assert_eq!(obfuscated_start(400, 0, None, Some(&cps)), 400);
}
#[test]
fn snapped_floor_conflict_takes_smallest_grid_point_in_range() {
assert_eq!(
obfuscated_start(1042, 950, Some(500), Some(&Snap::Spacing(100))),
1000
);
}
#[test]
fn jitter_with_spacing_stays_on_grid() {
for jitter in [0u64, 50, 99, 100, 250, 1000] {
let s = obfuscated_start(1042, 0, Some(jitter), Some(&Snap::Spacing(100)));
assert_eq!(s % 100, 0, "off-grid start {s} for jitter {jitter}");
assert!(s <= 1000);
}
}
#[test]
fn jitter_without_snap_respects_floor() {
assert_eq!(obfuscated_start(1000, 950, Some(200), None), 950);
assert_eq!(obfuscated_start(1000, 0, Some(200), None), 800);
assert_eq!(obfuscated_start(1000, 0, Some(0), None), 1000);
}
}