#![deny(clippy::arithmetic_side_effects)]
use super::build_id::{BLOCK_SIZE, BlockHasher, combine_digests};
use super::chunks::{ChunkRange, split_chunks};
use super::positional::{read_exact_at, write_all_at};
use crate::args::BuildId;
use crate::error::{Error, Result};
use rayon::prelude::*;
use std::fs::File;
use std::io;
use std::path::Path;
const BLOCK: u64 = BLOCK_SIZE as u64;
pub(super) const WRITE_PIECE: usize = 8 << 20;
const RETAIN_LIMIT: u64 = 64 << 10;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct HashPlan {
pub(super) kind: BuildId,
pub(super) field: ChunkRange,
}
#[derive(Debug)]
pub(super) struct Precomputed {
pub(super) plan: HashPlan,
parts: Vec<BlockPart>,
}
#[derive(Debug)]
enum BlockPart {
Digest(u64, Vec<u8>),
Open {
block: u64,
at: u64,
hasher: BlockHasher,
},
Piece {
block: u64,
offset: u64,
bytes: Vec<u8>,
},
Missing(u64),
}
#[derive(Clone, Copy, Debug)]
struct Region {
start: u64,
end: u64,
first: usize,
count: usize,
}
fn plan_regions(ranges: &[ChunkRange]) -> Vec<Region> {
let mut regions: Vec<Region> = Vec::new();
let mut previous_key = None;
for (index, range) in ranges.iter().enumerate() {
let last = range.offset.saturating_add(range.size.saturating_sub(1));
let key = last.checked_div(BLOCK).unwrap_or(0);
let end = range.offset.saturating_add(range.size);
match regions.last_mut() {
Some(region) if previous_key == Some(key) => {
region.end = region.end.max(end);
region.count = region.count.saturating_add(1);
}
_ => regions.push(Region {
start: range.offset,
end,
first: index,
count: 1,
}),
}
previous_key = Some(key);
}
regions
}
fn zero_overlap(buf: &mut [u8], start: u64, field: ChunkRange) {
let Some(field_end) = field.end() else {
return;
};
let buf_end = start.saturating_add(buf.len() as u64);
let lo = field.offset.max(start);
let hi = field_end.min(buf_end);
if lo >= hi {
return;
}
let (Ok(lo), Ok(hi)) = (
usize::try_from(lo.saturating_sub(start)),
usize::try_from(hi.saturating_sub(start)),
) else {
return;
};
if let Some(bytes) = buf.get_mut(lo..hi) {
bytes.fill(0);
}
}
fn io_error(path: &Path, error: io::Error) -> Error {
Error::io(path, error)
}
fn too_large() -> io::Error {
io::Error::new(
io::ErrorKind::OutOfMemory,
"output region is larger than the address space",
)
}
pub(super) fn write_buffer(file: &File, buf: &[u8], offset: u64) -> io::Result<()> {
if buf.len() <= WRITE_PIECE {
return write_all_at(file, buf, offset);
}
buf.par_chunks(WRITE_PIECE)
.enumerate()
.try_for_each(|(index, piece)| {
let at = (index as u64)
.checked_mul(WRITE_PIECE as u64)
.and_then(|delta| offset.checked_add(delta))
.ok_or_else(too_large)?;
write_all_at(file, piece, at)
})
}
pub(super) fn read_buffer(file: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
buf.par_chunks_mut(WRITE_PIECE)
.enumerate()
.try_for_each(|(index, piece)| {
let at = (index as u64)
.checked_mul(WRITE_PIECE as u64)
.and_then(|delta| offset.checked_add(delta))
.ok_or_else(too_large)?;
read_exact_at(file, piece, at)
})
}
struct RegionOutcome {
chunk_error: Option<Error>,
io_error: Option<io::Error>,
parts: Vec<BlockPart>,
}
pub(super) fn write_chunks<F>(
file: &File,
path: &Path,
len: u64,
ranges: &[ChunkRange],
write: F,
plan: Option<&HashPlan>,
) -> Result<Option<Precomputed>>
where
F: Fn(usize, &mut [u8]) -> Result<()> + Sync,
{
let regions = plan_regions(ranges);
let mut previous_end = vec![0u64; regions.len()];
let mut next_start = vec![len; regions.len()];
let mut end = 0u64;
for (index, region) in regions.iter().enumerate() {
if let Some(slot) = previous_end.get_mut(index) {
*slot = end;
}
if region.end > region.start {
end = region.end;
}
}
let mut start = len;
for (index, region) in regions.iter().enumerate().rev() {
if let Some(slot) = next_start.get_mut(index) {
*slot = start;
}
if region.end > region.start {
start = region.start;
}
}
let mut missing = Vec::new();
if plan.is_some() {
missing = vec![false; usize::try_from(len.div_ceil(BLOCK)).unwrap_or(0)];
for (index, region) in regions.iter().enumerate() {
if region.end <= region.start {
continue;
}
let block = region.start.checked_div(BLOCK).unwrap_or(0);
let (block_start, block_end) = block_span(block, len);
let previous = previous_end.get(index).copied().unwrap_or(0);
let head = block_end.min(region.end).saturating_sub(region.start);
if previous > block_start
&& head > RETAIN_LIMIT
&& let Some(slot) = usize::try_from(block).ok().and_then(|b| missing.get_mut(b))
{
*slot = true;
}
}
}
let outcomes: Vec<RegionOutcome> = regions
.par_iter()
.enumerate()
.map(|(index, region)| {
let context = RegionContext {
file,
ranges,
len,
previous_end: previous_end.get(index).copied().unwrap_or(0),
next_start: next_start.get(index).copied().unwrap_or(len),
missing: &missing,
plan,
};
run_region(&context, *region, &write)
})
.collect();
let mut io_failure = None;
let mut parts = Vec::new();
for outcome in outcomes {
if let Some(error) = outcome.chunk_error {
return Err(error);
}
if io_failure.is_none() {
io_failure = outcome.io_error;
}
parts.extend(outcome.parts);
}
if let Some(error) = io_failure {
return Err(io_error(path, error));
}
Ok(plan.map(|plan| Precomputed {
plan: plan.clone(),
parts,
}))
}
struct RegionContext<'a> {
file: &'a File,
ranges: &'a [ChunkRange],
len: u64,
previous_end: u64,
next_start: u64,
missing: &'a [bool],
plan: Option<&'a HashPlan>,
}
fn run_region<F>(context: &RegionContext<'_>, region: Region, write: &F) -> RegionOutcome
where
F: Fn(usize, &mut [u8]) -> Result<()> + Sync,
{
let failed = |error: io::Error| RegionOutcome {
chunk_error: None,
io_error: Some(error),
parts: Vec::new(),
};
let Ok(size) = usize::try_from(region.end.saturating_sub(region.start)) else {
return failed(too_large());
};
let chunks = region
.first
.checked_add(region.count)
.and_then(|end| context.ranges.get(region.first..end))
.unwrap_or(&[]);
let local: Vec<ChunkRange> = chunks
.iter()
.map(|range| ChunkRange::new(range.offset.saturating_sub(region.start), range.size))
.collect();
let mut buf = vec![0u8; size];
let slices = match split_chunks(&mut buf, &local) {
Ok(slices) => slices,
Err(error) => {
return RegionOutcome {
chunk_error: Some(Error::Internal(format!("invalid output layout: {error}"))),
io_error: None,
parts: Vec::new(),
};
}
};
let results: Vec<Result<()>> = slices
.into_par_iter()
.enumerate()
.map(|(index, chunk)| write(region.first.saturating_add(index), chunk))
.collect();
if let Some(error) = results.into_iter().find_map(Result::err) {
return RegionOutcome {
chunk_error: Some(error),
io_error: None,
parts: Vec::new(),
};
}
if let Err(error) = write_buffer(context.file, &buf, region.start) {
return failed(error);
}
let parts = match context.plan {
Some(plan) if size > 0 => {
zero_overlap(&mut buf, region.start, plan.field);
hash_region(context, plan, &buf, region.start)
}
_ => Vec::new(),
};
RegionOutcome {
chunk_error: None,
io_error: None,
parts,
}
}
fn block_span(block: u64, len: u64) -> (u64, u64) {
let start = block.saturating_mul(BLOCK);
(start, start.saturating_add(BLOCK).min(len))
}
fn hash_region(
context: &RegionContext<'_>,
plan: &HashPlan,
buf: &[u8],
start: u64,
) -> Vec<BlockPart> {
let end = start.saturating_add(buf.len() as u64);
let first = start.checked_div(BLOCK).unwrap_or(0);
let last = end.saturating_sub(1).checked_div(BLOCK).unwrap_or(0);
(first..=last)
.into_par_iter()
.filter_map(|block| {
if usize::try_from(block)
.ok()
.and_then(|b| context.missing.get(b))
.copied()
.unwrap_or(false)
{
return Some(BlockPart::Missing(block));
}
let (block_start, block_end) = block_span(block, context.len);
let lo = block_start.max(start);
let hi = block_end.min(end);
let data = buf.get(
usize::try_from(lo.saturating_sub(start)).ok()?
..usize::try_from(hi.saturating_sub(start)).ok()?,
)?;
if context.previous_end > block_start {
return Some(BlockPart::Piece {
block,
offset: lo,
bytes: data.to_vec(),
});
}
let mut hasher = BlockHasher::new(&plan.kind)?;
hasher.update_zeros(lo.saturating_sub(block_start));
hasher.update(data);
if hi == block_end {
return Some(BlockPart::Digest(block, hasher.finish()));
}
let until = block_end.min(context.next_start);
hasher.update_zeros(until.saturating_sub(hi));
Some(if until == block_end {
BlockPart::Digest(block, hasher.finish())
} else {
BlockPart::Open {
block,
at: until,
hasher,
}
})
})
.collect()
}
pub(super) fn finish_build_id(
file: &File,
len: u64,
kind: &BuildId,
field: ChunkRange,
precomputed: Option<Precomputed>,
) -> io::Result<Option<Vec<u8>>> {
if BlockHasher::new(kind).is_none() {
return Ok(None);
}
let blocks = usize::try_from(len.div_ceil(BLOCK)).map_err(|_| too_large())?;
let mut slots: Vec<Slot> = std::iter::repeat_with(Slot::default).take(blocks).collect();
match precomputed {
Some(precomputed) => {
for part in precomputed.parts {
let block = match &part {
BlockPart::Digest(block, _)
| BlockPart::Open { block, .. }
| BlockPart::Piece { block, .. }
| BlockPart::Missing(block) => *block,
};
let Some(slot) = usize::try_from(block).ok().and_then(|b| slots.get_mut(b)) else {
continue;
};
match part {
BlockPart::Digest(_, digest) => slot.digest = Some(digest),
BlockPart::Open { at, hasher, .. } => slot.open = Some((at, hasher)),
BlockPart::Piece { offset, bytes, .. } => slot.pieces.push((offset, bytes)),
BlockPart::Missing(_) => slot.missing = true,
}
}
}
None => slots.iter_mut().for_each(|slot| slot.missing = true),
}
let digests: Vec<Vec<u8>> = slots
.into_par_iter()
.enumerate()
.map(|(block, slot)| block_digest(file, len, kind, field, block as u64, slot))
.collect::<io::Result<_>>()?;
Ok(combine_digests(kind, &digests))
}
#[derive(Debug, Default)]
struct Slot {
digest: Option<Vec<u8>>,
open: Option<(u64, BlockHasher)>,
pieces: Vec<(u64, Vec<u8>)>,
missing: bool,
}
fn block_digest(
file: &File,
len: u64,
kind: &BuildId,
field: ChunkRange,
block: u64,
mut slot: Slot,
) -> io::Result<Vec<u8>> {
let (start, end) = block_span(block, len);
let fresh = || BlockHasher::new(kind).ok_or_else(|| io::Error::other("build-id kind"));
if slot.missing {
let size = usize::try_from(end.saturating_sub(start)).map_err(|_| too_large())?;
let mut buf = vec![0u8; size];
read_exact_at(file, &mut buf, start)?;
zero_overlap(&mut buf, start, field);
let mut hasher = fresh()?;
hasher.update(&buf);
return Ok(hasher.finish());
}
if let Some(digest) = slot.digest {
return Ok(digest);
}
let (mut at, mut hasher) = match slot.open {
Some(open) => open,
None => (start, fresh()?),
};
slot.pieces.sort_by_key(|(offset, _)| *offset);
for (offset, bytes) in &slot.pieces {
hasher.update_zeros(offset.saturating_sub(at));
hasher.update(bytes);
at = offset.saturating_add(bytes.len() as u64).max(at);
}
hasher.update_zeros(end.saturating_sub(at));
Ok(hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn regions_group_by_last_byte_block() {
let b = BLOCK;
let ranges = [
ChunkRange::new(0, 10),
ChunkRange::new(10, b - 10),
ChunkRange::new(b, 0),
ChunkRange::new(b + 5, b),
ChunkRange::new(2 * b + 5, 10),
ChunkRange::new(5 * b, 3 * b),
];
let regions = plan_regions(&ranges);
let spans: Vec<(u64, u64, usize, usize)> = regions
.iter()
.map(|r| (r.start, r.end, r.first, r.count))
.collect();
assert_eq!(
spans,
[
(0, b, 0, 2),
(b, b, 2, 1),
(b + 5, 2 * b + 15, 3, 2),
(5 * b, 8 * b, 5, 1)
]
);
}
#[test]
fn hashing_while_writing_covers_every_part() {
let b = BLOCK;
let len = 4 * b;
let ranges = [
ChunkRange::new(0, b - 1000),
ChunkRange::new(b - 990, 5000),
ChunkRange::new(2 * b - 100_000, 100_010),
ChunkRange::new(2 * b + 50, 30),
];
let path =
std::env::temp_dir().join(format!("qld-written-parts-{}.tmp", std::process::id()));
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&path)
.unwrap();
file.set_len(len).unwrap();
let plan = HashPlan {
kind: BuildId::Sha1,
field: ChunkRange::new(b - 5, 20),
};
let fill = |index: usize, chunk: &mut [u8]| {
for (i, byte) in chunk.iter_mut().enumerate() {
*byte = (index + i) as u8 | 1;
}
Ok(())
};
let pre = write_chunks(&file, &path, len, &ranges, fill, Some(&plan))
.unwrap()
.unwrap();
let has = |f: fn(&BlockPart) -> bool| pre.parts.iter().any(f);
assert!(has(|p| matches!(p, BlockPart::Digest(..))));
assert!(has(|p| matches!(p, BlockPart::Open { .. })));
assert!(has(|p| matches!(p, BlockPart::Piece { .. })));
assert!(has(|p| matches!(p, BlockPart::Missing(_))));
let mut image = std::fs::read(&path).unwrap();
image[(b - 5) as usize..(b + 15) as usize].fill(0);
let expected = super::super::build_id::compute_build_id(&BuildId::Sha1, &image);
let id = finish_build_id(&file, len, &BuildId::Sha1, plan.field, Some(pre)).unwrap();
assert_eq!(id, expected);
let id = finish_build_id(&file, len, &BuildId::Sha1, plan.field, None).unwrap();
assert_eq!(id, expected);
drop(file);
let _ = std::fs::remove_file(&path);
}
#[test]
fn zero_overlap_clips() {
let mut buf = [1u8; 10];
zero_overlap(&mut buf, 100, ChunkRange::new(95, 8));
assert_eq!(buf, [0, 0, 0, 1, 1, 1, 1, 1, 1, 1]);
let mut buf = [1u8; 10];
zero_overlap(&mut buf, 100, ChunkRange::new(108, 8));
assert_eq!(buf, [1, 1, 1, 1, 1, 1, 1, 1, 0, 0]);
zero_overlap(&mut buf, 100, ChunkRange::new(u64::MAX, 8));
}
}