#![deny(clippy::arithmetic_side_effects)]
use super::chunks::{ChunkRange, LayoutError};
use super::hash::{Md5, Sha1, Xxh64, xxh64};
pub use super::random::random_uuid;
use crate::args::BuildId;
use rayon::prelude::*;
pub const BLOCK_SIZE: usize = 1 << 20;
#[must_use]
pub fn build_id_size(kind: &BuildId) -> Option<usize> {
match kind {
BuildId::None => None,
BuildId::Fast => Some(8),
BuildId::Md5 | BuildId::Uuid => Some(Md5::LEN),
BuildId::Sha1 => Some(Sha1::LEN),
BuildId::Hex(bytes) => Some(bytes.len()),
}
}
#[must_use]
pub fn compute_build_id(kind: &BuildId, image: &[u8]) -> Option<Vec<u8>> {
match kind {
BuildId::None => None,
BuildId::Fast => Some(tree_hash(image, |data| xxh64(data, 0).to_be_bytes()).to_vec()),
BuildId::Md5 => Some(tree_hash(image, Md5::digest).to_vec()),
BuildId::Sha1 => Some(tree_hash(image, Sha1::digest).to_vec()),
BuildId::Uuid => Some(random_uuid().to_vec()),
BuildId::Hex(bytes) => Some(bytes.clone()),
}
}
pub fn apply_build_id(
kind: &BuildId,
image: &mut [u8],
offset: u64,
) -> Result<Option<Vec<u8>>, LayoutError> {
let Some(size) = build_id_size(kind) else {
return Ok(None);
};
let range = ChunkRange::new(offset, size as u64);
let len = image.len() as u64;
let out_of_bounds = || LayoutError::FieldOutOfBounds { range, len };
let start = usize::try_from(offset).map_err(|_| out_of_bounds())?;
let end = start.checked_add(size).ok_or_else(out_of_bounds)?;
image.get_mut(start..end).ok_or_else(out_of_bounds)?.fill(0);
let Some(id) = compute_build_id(kind, image) else {
return Ok(None);
};
if let Some(field) = image.get_mut(start..end)
&& field.len() == id.len()
{
field.copy_from_slice(&id);
}
Ok(Some(id))
}
#[derive(Clone)]
pub(super) enum BlockHasher {
Fast(Xxh64),
Md5(Md5),
Sha1(Sha1),
}
impl std::fmt::Debug for BlockHasher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Fast(_) => "BlockHasher::Fast",
Self::Md5(_) => "BlockHasher::Md5",
Self::Sha1(_) => "BlockHasher::Sha1",
})
}
}
impl BlockHasher {
pub(super) fn new(kind: &BuildId) -> Option<Self> {
match kind {
BuildId::Fast => Some(Self::Fast(Xxh64::new(0))),
BuildId::Md5 => Some(Self::Md5(Md5::new())),
BuildId::Sha1 => Some(Self::Sha1(Sha1::new())),
BuildId::None | BuildId::Uuid | BuildId::Hex(_) => None,
}
}
pub(super) fn update(&mut self, data: &[u8]) {
match self {
Self::Fast(h) => h.update(data),
Self::Md5(h) => h.update(data),
Self::Sha1(h) => h.update(data),
}
}
pub(super) fn update_zeros(&mut self, mut count: u64) {
const ZEROS: [u8; 4096] = [0; 4096];
while count > 0 {
let take = usize::try_from(count)
.unwrap_or(ZEROS.len())
.min(ZEROS.len());
self.update(ZEROS.get(..take).unwrap_or(&ZEROS));
count = count.saturating_sub(take as u64);
}
}
pub(super) fn finish(self) -> Vec<u8> {
match self {
Self::Fast(h) => h.finish().to_be_bytes().to_vec(),
Self::Md5(h) => h.finalize().to_vec(),
Self::Sha1(h) => h.finalize().to_vec(),
}
}
}
pub(super) fn combine_digests(kind: &BuildId, digests: &[Vec<u8>]) -> Option<Vec<u8>> {
let mut hasher = BlockHasher::new(kind)?;
for digest in digests {
hasher.update(digest);
}
Some(hasher.finish())
}
fn tree_hash<const N: usize>(image: &[u8], leaf: impl Fn(&[u8]) -> [u8; N] + Sync) -> [u8; N] {
let digests: Vec<[u8; N]> = image.par_chunks(BLOCK_SIZE).map(&leaf).collect();
leaf(digests.as_flattened())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sizes() {
assert_eq!(build_id_size(&BuildId::None), None);
assert_eq!(build_id_size(&BuildId::Fast), Some(8));
assert_eq!(build_id_size(&BuildId::Md5), Some(16));
assert_eq!(build_id_size(&BuildId::Sha1), Some(20));
assert_eq!(build_id_size(&BuildId::Uuid), Some(16));
assert_eq!(build_id_size(&BuildId::Hex(vec![1, 2, 3])), Some(3));
}
#[test]
fn tree_layout_is_hash_of_block_hashes() {
let image: Vec<u8> = (0..(BLOCK_SIZE * 2 + 5)).map(|i| (i % 251) as u8).collect();
let mut concat = Vec::new();
for block in image.chunks(BLOCK_SIZE) {
concat.extend_from_slice(&Sha1::digest(block));
}
assert_eq!(
compute_build_id(&BuildId::Sha1, &image).unwrap(),
Sha1::digest(&concat)
);
let mut concat = Vec::new();
for block in image.chunks(BLOCK_SIZE) {
concat.extend_from_slice(&xxh64(block, 0).to_be_bytes());
}
assert_eq!(
compute_build_id(&BuildId::Fast, &image).unwrap(),
xxh64(&concat, 0).to_be_bytes()
);
}
#[test]
fn empty_image_values_are_stable() {
assert_eq!(
compute_build_id(&BuildId::Fast, &[]).unwrap(),
0xef46_db37_51d8_e999u64.to_be_bytes()
);
assert_eq!(
compute_build_id(&BuildId::Md5, &[]).unwrap(),
Md5::digest(b"")
);
}
#[test]
fn apply_zeroes_field_before_hashing() {
let mut a = vec![7u8; 4096];
let mut b = a.clone();
b[100..120].fill(0xee);
let id_a = apply_build_id(&BuildId::Sha1, &mut a, 100)
.unwrap()
.unwrap();
let id_b = apply_build_id(&BuildId::Sha1, &mut b, 100)
.unwrap()
.unwrap();
assert_eq!(id_a, id_b);
assert_eq!(a, b);
assert_eq!(&a[100..120], &id_a[..]);
let mut expected = vec![7u8; 4096];
expected[100..120].fill(0);
assert_eq!(compute_build_id(&BuildId::Sha1, &expected).unwrap(), id_a);
}
#[test]
fn piecewise_block_hashes_match() {
let image: Vec<u8> = (0..(BLOCK_SIZE * 3 + 77))
.map(|i| (i % 253) as u8)
.collect();
for kind in [BuildId::Fast, BuildId::Md5, BuildId::Sha1] {
let digests: Vec<Vec<u8>> = image
.chunks(BLOCK_SIZE)
.map(|block| {
let mut hasher = BlockHasher::new(&kind).unwrap();
let (a, b) = block.split_at(block.len() / 3);
hasher.update(a);
hasher.update(b);
hasher.finish()
})
.collect();
assert_eq!(
combine_digests(&kind, &digests),
compute_build_id(&kind, &image),
"{kind:?}"
);
}
let mut zeros = BlockHasher::new(&BuildId::Sha1).unwrap();
zeros.update_zeros(10_000);
assert_eq!(zeros.finish(), Sha1::digest(&[0; 10_000]));
}
#[test]
fn apply_hex_and_none() {
let mut image = vec![0u8; 16];
let id = apply_build_id(&BuildId::Hex(vec![0xde, 0xad]), &mut image, 14).unwrap();
assert_eq!(id, Some(vec![0xde, 0xad]));
assert_eq!(&image[14..], [0xde, 0xad]);
assert_eq!(apply_build_id(&BuildId::None, &mut image, 1000), Ok(None));
}
#[test]
fn apply_rejects_out_of_bounds_fields() {
let mut image = vec![1u8; 32];
for offset in [13, 32, u64::MAX] {
assert!(matches!(
apply_build_id(&BuildId::Sha1, &mut image, offset),
Err(LayoutError::FieldOutOfBounds { .. })
));
}
assert_eq!(image, vec![1u8; 32]);
}
}