use crate::archive::index::ArchiveIndex;
use crate::archive::types::{
BlockDescriptor, BlockHash, BlockLocation, BlockReferences, ContentState, Entry,
ExternalLocation, FileId, ReadRange,
};
use crate::block;
use crate::error::PithosError;
use std::ops::Range;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlannedBlock {
pub(crate) file: FileId,
pub(crate) position: usize,
pub(crate) hash: BlockHash,
pub(crate) descriptor: BlockDescriptor,
pub(crate) output: Range<usize>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BlockRequest {
Local { offset: u64, len: u64 },
External {
location: ExternalLocation,
len: u64,
},
}
impl PlannedBlock {
pub fn request(&self) -> BlockRequest {
let len = self.framed_len();
match &self.descriptor.location {
BlockLocation::Local(span) => BlockRequest::Local {
offset: span.start(),
len,
},
BlockLocation::External(location) => BlockRequest::External {
location: location.clone(),
len,
},
}
}
pub fn output(&self) -> Range<usize> {
self.output.clone()
}
fn local_span(&self) -> Option<(u64, u64)> {
match &self.descriptor.location {
BlockLocation::Local(span) => Some((span.start(), self.framed_len())),
BlockLocation::External(_) => None,
}
}
pub(crate) fn framed_len(&self) -> u64 {
self.descriptor.stored_size.saturating_add(4)
}
pub(crate) fn repeats(&self, previous: &Self) -> bool {
self.descriptor.location == previous.descriptor.location
&& self.framed_len() == previous.framed_len()
}
pub(crate) fn same_block(&self, other: &Self) -> bool {
self.file == other.file && self.hash == other.hash && self.descriptor == other.descriptor
}
}
pub struct ReadPlan<'a> {
index: &'a ArchiveIndex,
file: FileId,
references: std::iter::Enumerate<std::slice::Iter<'a, BlockHash>>,
pub(super) first_position: usize,
cursor: u64,
range: ReadRange,
limits: block::Limits,
}
impl<'a> ReadPlan<'a> {
pub(crate) fn new(
index: &'a ArchiveIndex,
file: FileId,
range: ReadRange,
limits: block::Limits,
) -> Result<Self, PithosError> {
let references = available_references(index, file)?;
let (first_position, cursor) = references.start_near(range.start());
let references = if range.start() < range.end() {
&references.as_slice()[first_position..]
} else {
&[]
};
Ok(Self {
index,
file,
references: references.iter().enumerate(),
first_position,
cursor,
range,
limits,
})
}
#[cfg(feature = "async")]
fn resume(
index: &'a ArchiveIndex,
file: FileId,
range: ReadRange,
limits: block::Limits,
position: Option<usize>,
cursor: u64,
) -> Result<Self, PithosError> {
let references = match position {
Some(position) => available_references(index, file)?
.as_slice()
.get(position..)
.unwrap_or_default(),
None => &[],
};
Ok(Self {
index,
file,
references: references.iter().enumerate(),
first_position: position.unwrap_or_default(),
cursor,
range,
limits,
})
}
#[cfg(feature = "async")]
fn position(&self) -> Option<usize> {
let (offset, _) = self.references.clone().next()?;
Some(self.first_position + offset)
}
fn step(
&mut self,
position: usize,
hash: BlockHash,
) -> Result<Option<PlannedBlock>, PithosError> {
let descriptor = self
.index
.descriptor(hash)
.ok_or(PithosError::MissingBlockDescriptor)?;
let start = self.cursor;
let end = start.checked_add(descriptor.original_size).ok_or(
PithosError::InvalidDirectoryRange {
operation: "sum block sizes",
},
)?;
self.cursor = end;
if start >= self.range.end() {
self.references = [].iter().enumerate();
return Ok(None);
}
if end <= self.range.start() {
return Ok(None);
}
check_block_limits(descriptor, self.limits)?;
let convert = |value: u64| {
usize::try_from(value).map_err(|_| PithosError::InvalidDirectoryRange {
operation: "convert range index",
})
};
let output_start = convert(self.range.start().saturating_sub(start))?;
let output_end = convert(self.range.end().min(end) - start)?;
Ok(Some(PlannedBlock {
file: self.file,
position,
hash,
descriptor: descriptor.clone(),
output: output_start..output_end,
}))
}
}
fn available_references(
index: &ArchiveIndex,
file: FileId,
) -> Result<&BlockReferences, PithosError> {
let content = index
.entry(file)
.and_then(|entry| match &entry.entry {
Entry::File(content) | Entry::Metadata(content) => Some(content),
Entry::Directory(_) | Entry::Symlink { .. } => None,
})
.ok_or_else(|| {
PithosError::InvalidBlockDataState("only data/metadata entries have content".into())
})?;
match &content.content {
ContentState::Available(references) => Ok(references),
ContentState::Unavailable => Err(PithosError::ContentUnavailable),
}
}
pub(crate) fn check_block_limits(
descriptor: &BlockDescriptor,
limits: block::Limits,
) -> Result<(), PithosError> {
if descriptor.stored_size > limits.max_stored_bytes {
return Err(PithosError::LimitExceeded {
field: "stored block",
limit: limits.max_stored_bytes,
actual: descriptor.stored_size,
});
}
if descriptor.original_size > limits.max_decoded_bytes {
return Err(PithosError::LimitExceeded {
field: "decoded block",
limit: limits.max_decoded_bytes,
actual: descriptor.original_size,
});
}
Ok(())
}
impl Iterator for ReadPlan<'_> {
type Item = Result<PlannedBlock, PithosError>;
fn next(&mut self) -> Option<Self::Item> {
while let Some((offset, &hash)) = self.references.next() {
match self.step(self.first_position + offset, hash) {
Ok(Some(block)) => return Some(Ok(block)),
Ok(None) => {}
Err(error) => {
self.references = [].iter().enumerate();
return Some(Err(error));
}
}
}
None
}
}
impl<'a> ReadPlan<'a> {
pub fn batches(self, max_bytes: u64) -> BlockBatches<'a> {
BlockBatches {
plan: self,
max_bytes,
pending: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BlockBatch {
blocks: Vec<PlannedBlock>,
}
impl BlockBatch {
pub fn request(&self) -> BlockRequest {
let mut request = self.blocks[0].request();
if let BlockRequest::Local { len, .. } = &mut request {
*len = self.stored_len();
}
request
}
fn stored_len(&self) -> u64 {
let mut previous: Option<&PlannedBlock> = None;
self.blocks
.iter()
.filter(|block| {
let repeat = previous.is_some_and(|previous| block.repeats(previous));
previous = Some(block);
!repeat
})
.map(PlannedBlock::framed_len)
.sum()
}
pub fn blocks(&self) -> &[PlannedBlock] {
&self.blocks
}
pub fn split<'b>(
&'b self,
response: &'b [u8],
) -> Result<impl Iterator<Item = (&'b PlannedBlock, &'b [u8])>, PithosError> {
let expected = self.stored_len();
if response.len() as u64 != expected {
return Err(PithosError::BlockSizeMismatch {
expected,
actual: response.len() as u64,
});
}
let mut rest = response;
let mut last: Option<(&PlannedBlock, &[u8])> = None;
Ok(self.blocks.iter().map(move |block| {
if let Some((previous, stored)) = last
&& block.repeats(previous)
{
return (block, stored);
}
let (stored, tail) = rest.split_at(block.framed_len() as usize);
rest = tail;
last = Some((block, stored));
(block, stored)
}))
}
}
pub struct BlockBatches<'a> {
plan: ReadPlan<'a>,
max_bytes: u64,
pending: Option<Result<PlannedBlock, PithosError>>,
}
pub const MAX_BATCH_BLOCKS: usize = 1024;
impl Iterator for BlockBatches<'_> {
type Item = Result<BlockBatch, PithosError>;
fn next(&mut self) -> Option<Self::Item> {
let first = match self.pending.take().or_else(|| self.plan.next())? {
Ok(block) => block,
Err(error) => return Some(Err(error)),
};
let Some((offset, len)) = first.local_span() else {
let mut blocks = vec![first];
while blocks.len() < MAX_BATCH_BLOCKS {
match self.plan.next() {
Some(Ok(next)) if next.repeats(&blocks[0]) => blocks.push(next),
Some(next) => {
self.pending = Some(next);
break;
}
None => break,
}
}
return Some(Ok(BlockBatch { blocks }));
};
let (mut last, mut end, mut total) = (offset, offset + len, len);
let mut blocks = vec![first];
while blocks.len() < MAX_BATCH_BLOCKS {
let Some(next) = self.plan.next() else {
break;
};
match next.as_ref().map(PlannedBlock::local_span) {
Ok(Some((offset, len))) if offset == last && offset + len == end => {
blocks.extend(next.ok());
}
Ok(Some((offset, len))) if offset == end && total + len <= self.max_bytes => {
last = offset;
end += len;
total += len;
blocks.extend(next.ok());
}
_ => {
self.pending = Some(next);
break;
}
}
}
Some(Ok(BlockBatch { blocks }))
}
}
#[cfg(feature = "async")]
impl BlockBatches<'_> {
pub(crate) fn detach(self) -> BatchCursor {
BatchCursor {
file: self.plan.file,
range: self.plan.range,
limits: self.plan.limits,
position: self.plan.position(),
cursor: self.plan.cursor,
max_bytes: self.max_bytes,
pending: self.pending,
}
}
}
#[cfg(feature = "async")]
pub(crate) struct BatchCursor {
file: FileId,
range: ReadRange,
limits: block::Limits,
position: Option<usize>,
cursor: u64,
max_bytes: u64,
pending: Option<Result<PlannedBlock, PithosError>>,
}
#[cfg(feature = "async")]
impl BatchCursor {
pub(crate) fn next_batch(
&mut self,
index: &ArchiveIndex,
) -> Option<Result<BlockBatch, PithosError>> {
let plan = ReadPlan::resume(
index,
self.file,
self.range,
self.limits,
self.position,
self.cursor,
);
let plan = match plan {
Ok(plan) => plan,
Err(error) => {
self.position = None;
self.pending = None;
return Some(Err(error));
}
};
let mut batches = BlockBatches {
plan,
max_bytes: self.max_bytes,
pending: self.pending.take(),
};
let batch = batches.next();
*self = batches.detach();
batch
}
}