use crate::error::VerifyError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChunkLayout {
chunk_lens: Vec<u64>,
offsets: Vec<u64>,
}
pub const MAX_RESOURCE_CHUNK_COUNT: usize = 1024 * 1024;
impl ChunkLayout {
pub fn new(chunk_lens: Vec<u64>) -> Self {
let mut offsets = Vec::with_capacity(chunk_lens.len() + 1);
let mut acc = 0u64;
offsets.push(0);
for &len in &chunk_lens {
acc = acc.saturating_add(len);
offsets.push(acc);
}
ChunkLayout {
chunk_lens,
offsets,
}
}
pub fn try_new(chunk_lens: Vec<u64>) -> Result<Self, VerifyError> {
if chunk_lens.len() > MAX_RESOURCE_CHUNK_COUNT {
return Err(VerifyError::Metadata(format!(
"declared chunk_lens count {} exceeds the maximum {MAX_RESOURCE_CHUNK_COUNT}",
chunk_lens.len()
)));
}
let mut offsets: Vec<u64> = Vec::new();
offsets
.try_reserve_exact(chunk_lens.len() + 1)
.map_err(|e| {
VerifyError::Metadata(format!(
"cannot allocate a {}-entry chunk layout: {e}",
chunk_lens.len() + 1
))
})?;
let mut acc = 0u64;
offsets.push(0);
for &len in &chunk_lens {
acc = acc.checked_add(len).ok_or_else(|| {
VerifyError::Metadata(
"chunk_lens cumulative length overflows u64 (hostile metadata)".into(),
)
})?;
offsets.push(acc);
}
Ok(ChunkLayout {
chunk_lens,
offsets,
})
}
pub fn chunk_count(&self) -> usize {
self.chunk_lens.len()
}
pub fn chunk_lens(&self) -> &[u64] {
&self.chunk_lens
}
pub fn total_length(&self) -> u64 {
*self.offsets.last().unwrap_or(&0)
}
pub fn chunk_offset(&self, index: usize) -> Option<u64> {
self.offsets.get(index).copied()
}
pub fn chunk_len(&self, index: usize) -> Option<u64> {
self.chunk_lens.get(index).copied()
}
pub fn chunks_for_range(
&self,
offset: u64,
length: u64,
) -> Result<(usize, usize), VerifyError> {
let end = offset.saturating_add(length);
let start_idx = self
.offsets
.iter()
.position(|&o| o == offset)
.ok_or_else(|| {
VerifyError::Alignment(format!("offset {offset} is not a chunk boundary"))
})?;
let end_idx =
self.offsets.iter().position(|&o| o == end).ok_or_else(|| {
VerifyError::Alignment(format!("end {end} is not a chunk boundary"))
})?;
if end_idx < start_idx {
return Err(VerifyError::Alignment(format!(
"range end {end} precedes start {offset}"
)));
}
Ok((start_idx, end_idx))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Range {
pub index: usize,
pub offset: u64,
pub length: u64,
pub chunk_start: usize,
pub chunk_end: usize,
}
impl Range {
pub fn chunk_range(&self) -> std::ops::Range<usize> {
self.chunk_start..self.chunk_end
}
}
pub fn plan_ranges(layout: &ChunkLayout, window: u64) -> Vec<Range> {
let window = window.max(1);
let mut ranges = Vec::new();
let mut i = 0usize;
let n = layout.chunk_count();
while i < n {
let chunk_start = i;
let offset = layout.chunk_offset(i).unwrap_or(0);
let mut length = 0u64;
while i < n {
let clen = layout.chunk_len(i).unwrap_or(0);
if length > 0 && length.saturating_add(clen) > window {
break;
}
length = length.saturating_add(clen);
i += 1;
}
ranges.push(Range {
index: ranges.len(),
offset,
length,
chunk_start,
chunk_end: i,
});
}
ranges
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RangeState {
Pending,
InFlight(String),
Done,
}
impl RangeState {
pub fn is_incomplete(&self) -> bool {
!matches!(self, RangeState::Done)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_offsets_and_total() {
let l = ChunkLayout::new(vec![10, 20, 5]);
assert_eq!(l.chunk_count(), 3);
assert_eq!(l.total_length(), 35);
assert_eq!(l.chunk_offset(0), Some(0));
assert_eq!(l.chunk_offset(1), Some(10));
assert_eq!(l.chunk_offset(2), Some(30));
assert_eq!(l.chunk_offset(3), Some(35));
assert_eq!(l.chunk_offset(4), None);
assert_eq!(l.chunk_len(1), Some(20));
assert_eq!(l.chunk_len(9), None);
}
#[test]
fn chunks_for_aligned_range() {
let l = ChunkLayout::new(vec![10, 20, 5]);
assert_eq!(l.chunks_for_range(0, 30).unwrap(), (0, 2));
assert_eq!(l.chunks_for_range(10, 25).unwrap(), (1, 3));
assert_eq!(l.chunks_for_range(30, 5).unwrap(), (2, 3));
assert_eq!(l.chunks_for_range(0, 35).unwrap(), (0, 3));
}
#[test]
fn unaligned_range_rejected() {
let l = ChunkLayout::new(vec![10, 20, 5]);
assert!(matches!(
l.chunks_for_range(5, 10),
Err(VerifyError::Alignment(_))
));
assert!(matches!(
l.chunks_for_range(0, 15),
Err(VerifyError::Alignment(_))
));
}
#[test]
fn plan_packs_chunks_into_windows() {
let l = ChunkLayout::new(vec![10, 10, 10, 10]);
let ranges = plan_ranges(&l, 25);
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].offset, 0);
assert_eq!(ranges[0].length, 20);
assert_eq!(ranges[0].chunk_range(), 0..2);
assert_eq!(ranges[1].offset, 20);
assert_eq!(ranges[1].length, 20);
assert_eq!(ranges[1].chunk_range(), 2..4);
assert_eq!(
ranges.iter().map(|r| r.length).sum::<u64>(),
l.total_length()
);
}
#[test]
fn plan_oversized_chunk_is_its_own_range() {
let l = ChunkLayout::new(vec![100, 5]);
let ranges = plan_ranges(&l, 25);
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].length, 100); assert_eq!(ranges[0].chunk_range(), 0..1);
assert_eq!(ranges[1].length, 5);
assert_eq!(ranges[1].chunk_range(), 1..2);
}
#[test]
fn plan_single_range_when_window_large() {
let l = ChunkLayout::new(vec![10, 20, 5]);
let ranges = plan_ranges(&l, 1_000_000);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].offset, 0);
assert_eq!(ranges[0].length, 35);
assert_eq!(ranges[0].index, 0);
}
#[test]
fn plan_empty_resource_has_no_ranges() {
let l = ChunkLayout::new(vec![]);
assert!(plan_ranges(&l, 100).is_empty());
assert_eq!(l.total_length(), 0);
}
#[test]
fn range_state_incompleteness() {
assert!(RangeState::Pending.is_incomplete());
assert!(RangeState::InFlight("p".into()).is_incomplete());
assert!(!RangeState::Done.is_incomplete());
}
}