use std::ops::Range;
use crate::Result;
pub(super) trait ChunkSource {
fn read_chunk(&mut self, position: u64, buf: &mut [u8]) -> Result<usize>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ChunkEnd {
More,
Last,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Flow {
Next,
NeedMore,
Stop,
Finish,
}
#[derive(Debug)]
pub(super) struct Hit<'a, K> {
pub(super) offset: u64,
pub(super) bytes: &'a [u8],
pub(super) kind: K,
pub(super) end: ChunkEnd,
}
#[derive(Debug, Clone)]
pub(super) struct ChunkPlan {
pub(super) range: Range<u64>,
pub(super) chunk: usize,
pub(super) signature_len: usize,
pub(super) max_lookahead: usize,
}
pub(super) fn search_chunks<S, K>(
source: &mut S,
plan: &ChunkPlan,
mut find: impl FnMut(&[u8]) -> Option<(usize, K)>,
mut judge: impl FnMut(&mut S, Hit<'_, K>) -> Result<Flow>,
) -> Result<()>
where
S: ChunkSource + ?Sized,
{
let range = &plan.range;
if range.end.saturating_sub(range.start) < plan.signature_len as u64 {
return Ok(());
}
let chunk = plan.chunk.max(1);
let carried_signature = plan.signature_len.saturating_sub(1);
let mut buf: Vec<u8> = Vec::new();
let mut base = range.start;
loop {
let held = buf.len();
let read_from = base + held as u64;
let want =
usize::try_from(range.end.saturating_sub(read_from)).map_or(chunk, |n| n.min(chunk));
buf.resize(held + want, 0);
let got = source.read_chunk(read_from, &mut buf[held..])?;
buf.truncate(held + got);
let last = got < want || read_from + got as u64 >= range.end;
let end = if last { ChunkEnd::Last } else { ChunkEnd::More };
let mut keep_from = buf.len().saturating_sub(carried_signature);
let mut finish = false;
let mut next = 0;
while let Some((relative, kind)) = find(&buf[next..]) {
let index = next + relative;
next = index + 1;
let hit = Hit {
offset: base + index as u64,
bytes: &buf[index..],
kind,
end,
};
match judge(source, hit)? {
Flow::Next => {}
Flow::NeedMore => {
if !last && buf.len() - index < plan.max_lookahead + plan.signature_len {
keep_from = keep_from.min(index);
break;
}
}
Flow::Stop => return Ok(()),
Flow::Finish => finish = true,
}
}
if last || finish {
return Ok(());
}
buf.drain(..keep_from);
base += keep_from as u64;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use proptest::prelude::*;
use super::*;
struct Memory<'a>(&'a [u8]);
impl ChunkSource for Memory<'_> {
fn read_chunk(&mut self, position: u64, buf: &mut [u8]) -> Result<usize> {
let start = usize::try_from(position).unwrap();
let end = start + buf.len();
buf.copy_from_slice(&self.0[start..end]);
Ok(buf.len())
}
}
const SIGNATURES: [[u8; 4]; 2] = [*b"PK\x03\x04", *b"PK\x01\x02"];
fn find(bytes: &[u8]) -> Option<(usize, usize)> {
bytes
.array_windows::<4>()
.enumerate()
.find_map(|(index, w)| {
SIGNATURES
.iter()
.position(|s| w == s)
.map(|which| (index, which))
})
}
fn naive(data: &[u8], need: usize) -> Vec<(u64, usize)> {
let mut hits = Vec::new();
let mut at = 0;
while let Some((relative, which)) = find(&data[at..]) {
let index = at + relative;
if data.len() - index >= need {
hits.push((index as u64, which));
}
at = index + 1;
}
hits
}
fn chunked(data: &[u8], chunk: usize, need: usize) -> Vec<(u64, usize)> {
let plan = ChunkPlan {
range: 0..data.len() as u64,
chunk,
signature_len: 4,
max_lookahead: need,
};
let mut hits = Vec::new();
search_chunks(&mut Memory(data), &plan, find, |_, hit| {
if hit.bytes.len() < need {
return Ok(if hit.end == ChunkEnd::Last {
Flow::Next
} else {
Flow::NeedMore
});
}
hits.push((hit.offset, hit.kind));
Ok(Flow::Next)
})
.unwrap();
hits
}
fn stream() -> impl Strategy<Value = Vec<u8>> {
let piece = prop_oneof![
Just(b"PK\x03\x04".to_vec()),
Just(b"PK\x01\x02".to_vec()),
Just(b"PK\x03PK\x03\x04".to_vec()),
Just(b"PK".to_vec()),
prop::collection::vec(any::<u8>(), 0..24),
];
prop::collection::vec(piece, 0..8).prop_map(|pieces| pieces.concat())
}
proptest! {
#[test]
fn chunked_hits_equal_the_unchunked_scan(
data in stream(),
chunk in 1usize..=64,
need in prop::sample::select(vec![4usize, 30, 100]),
) {
prop_assert_eq!(chunked(&data, chunk, need), naive(&data, need));
}
}
#[test]
fn a_signature_is_found_at_every_offset_around_a_chunk_end() {
let chunk = 64;
for offset in chunk - 40..chunk + 40 {
let mut data = vec![0u8; 200];
data[offset..offset + 4].copy_from_slice(&SIGNATURES[0]);
assert_eq!(
chunked(&data, chunk, 30),
vec![(offset as u64, 0)],
"{offset}"
);
}
}
#[test]
fn an_empty_or_short_range_reads_nothing() {
struct Never;
impl ChunkSource for Never {
fn read_chunk(&mut self, _: u64, _: &mut [u8]) -> Result<usize> {
Err(crate::ArchiveError::InvalidArchive(
"unexpected read".into(),
))
}
}
let plan = ChunkPlan {
range: 5..8,
chunk: 16,
signature_len: 4,
max_lookahead: 30,
};
search_chunks(&mut Never, &plan, find, |_, _| Ok(Flow::Next)).unwrap();
}
}