use std::collections::BTreeMap;
use crate::btree::Tree;
use crate::chunk::DiskKey;
use crate::error::Result;
pub const CSUM_TREE_OBJECTID: u64 = 7;
pub const EXTENT_CSUM_KEY: u8 = 128;
pub const EXTENT_CSUM_OBJECTID: u64 = u64::MAX - 9;
pub type SectorDigests = BTreeMap<u64, Vec<u8>>;
pub(crate) fn digests_for_range(
tree: &Tree,
root: u64,
csum_size: usize,
sectorsize: u64,
start: u64,
len: u64,
) -> Result<SectorDigests> {
let mut out = SectorDigests::new();
if len == 0 || csum_size == 0 || sectorsize == 0 {
return Ok(out);
}
let end = start.saturating_add(len);
let target = DiskKey {
objectid: EXTENT_CSUM_OBJECTID,
key_type: EXTENT_CSUM_KEY,
offset: start,
};
let mut from = target;
let leaf = tree.descend(root, &target)?;
if let Some(items) = leaf.body.items() {
for item in items {
if item.key.objectid != EXTENT_CSUM_OBJECTID
|| item.key.key_type != EXTENT_CSUM_KEY
|| item.key.offset > start
{
continue;
}
if item.key.offset >= from.offset || from.offset == start {
from = item.key;
}
}
}
tree.for_each_from(root, &from, &mut |key: &DiskKey, data: &[u8]| {
if key.objectid != EXTENT_CSUM_OBJECTID || key.key_type != EXTENT_CSUM_KEY {
return Ok(false);
}
if key.offset >= end {
return Ok(false);
}
for (i, digest) in data.chunks_exact(csum_size).enumerate() {
let sector = key.offset.saturating_add(i as u64 * sectorsize);
if sector >= end {
break;
}
if sector >= start {
out.insert(sector, digest.to_vec());
}
}
Ok(true)
})?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::btree::test_blocks::{geom, key, leaf, put_key, LEAF_A};
fn tree_over(block: Vec<u8>) -> (Vec<u8>, u64) {
(block, LEAF_A)
}
fn four_sector_item(first_logical: u64) -> (DiskKey, Vec<u8>) {
let mut data = Vec::new();
for i in 0..4u8 {
data.extend_from_slice(&[0xA0 + i; 4]);
}
(
key(EXTENT_CSUM_OBJECTID, EXTENT_CSUM_KEY, first_logical),
data,
)
}
#[test]
fn a_sector_inside_a_run_gets_the_digest_from_the_item_that_starts_before_it() {
let sectorsize = 4096u64;
let base = 1 << 20;
let (k, data) = four_sector_item(base);
let (block, at) = tree_over(leaf(LEAF_A, CSUM_TREE_OBJECTID, &[(k, data)]));
let read = |logical: u64, buf: &mut [u8]| -> Result<()> {
assert_eq!(logical, at);
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let third = base + 2 * sectorsize;
let found = digests_for_range(&tree, at, 4, sectorsize, third, sectorsize).unwrap();
assert_eq!(found.len(), 1, "the covering item was not found");
assert_eq!(found[&third], vec![0xA2; 4]);
}
#[test]
fn a_run_yields_one_digest_per_sector() {
let sectorsize = 4096u64;
let base = 1 << 20;
let (k, data) = four_sector_item(base);
let (block, at) = tree_over(leaf(LEAF_A, CSUM_TREE_OBJECTID, &[(k, data)]));
let read = |_logical: u64, buf: &mut [u8]| -> Result<()> {
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let found = digests_for_range(&tree, at, 4, sectorsize, base, 4 * sectorsize).unwrap();
assert_eq!(found.len(), 4);
for i in 0..4u64 {
assert_eq!(found[&(base + i * sectorsize)], vec![0xA0 + i as u8; 4]);
}
}
#[test]
fn a_sector_no_item_covers_is_absent_rather_than_zero() {
let sectorsize = 4096u64;
let base = 1 << 20;
let (k, data) = four_sector_item(base);
let (block, at) = tree_over(leaf(LEAF_A, CSUM_TREE_OBJECTID, &[(k, data)]));
let read = |_logical: u64, buf: &mut [u8]| -> Result<()> {
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let past = base + 4 * sectorsize;
let found = digests_for_range(&tree, at, 4, sectorsize, past, sectorsize).unwrap();
assert!(
found.is_empty(),
"invented a digest for an uncovered sector"
);
}
#[test]
fn the_digest_length_decides_how_many_sectors_an_item_covers() {
let sectorsize = 4096u64;
let base = 1 << 20;
let mut data = Vec::new();
data.extend_from_slice(&[0x11u8; 32]);
data.extend_from_slice(&[0x22u8; 32]);
let k = key(EXTENT_CSUM_OBJECTID, EXTENT_CSUM_KEY, base);
let (block, at) = tree_over(leaf(LEAF_A, CSUM_TREE_OBJECTID, &[(k, data)]));
let read = |_logical: u64, buf: &mut [u8]| -> Result<()> {
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let found = digests_for_range(&tree, at, 32, sectorsize, base, 4 * sectorsize).unwrap();
assert_eq!(found.len(), 2, "64 bytes of sha256 is two sectors");
assert_eq!(found[&base], vec![0x11; 32]);
assert_eq!(found[&(base + sectorsize)], vec![0x22; 32]);
}
#[test]
fn items_of_another_type_are_not_read_as_digests() {
let sectorsize = 4096u64;
let base = 1 << 20;
let k = key(EXTENT_CSUM_OBJECTID, EXTENT_CSUM_KEY - 1, base);
let (block, at) = tree_over(leaf(LEAF_A, CSUM_TREE_OBJECTID, &[(k, vec![0xFF; 16])]));
let read = |_logical: u64, buf: &mut [u8]| -> Result<()> {
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
};
let tree = Tree::new(geom(), &read);
let found = digests_for_range(&tree, at, 4, sectorsize, base, sectorsize).unwrap();
assert!(found.is_empty(), "read a non-csum item as digests");
}
#[test]
fn key_encoding_round_trips() {
let mut b = vec![0u8; 32];
let k = key(EXTENT_CSUM_OBJECTID, EXTENT_CSUM_KEY, 4096);
put_key(&mut b, 0, &k);
assert_eq!(b[0..8], EXTENT_CSUM_OBJECTID.to_le_bytes());
assert_eq!(b[8], EXTENT_CSUM_KEY);
}
}
#[cfg(test)]
mod cross_leaf {
use super::*;
use crate::btree::test_blocks::{geom, key, leaf, node, LEAF_A, LEAF_B, ROOT};
use crate::btree::Tree;
use std::collections::HashMap;
fn two_leaf_tree(base: u64, sectorsize: u64) -> (HashMap<u64, Vec<u8>>, u64) {
let mut a_data = Vec::new();
let mut b_data = Vec::new();
for i in 0..4u8 {
a_data.extend_from_slice(&[0xA0 + i; 4]);
b_data.extend_from_slice(&[0xB0 + i; 4]);
}
let a_key = key(EXTENT_CSUM_OBJECTID, EXTENT_CSUM_KEY, base);
let b_key = key(EXTENT_CSUM_OBJECTID, EXTENT_CSUM_KEY, base + 4 * sectorsize);
let a = leaf(LEAF_A, CSUM_TREE_OBJECTID, &[(a_key, a_data)]);
let b = leaf(LEAF_B, CSUM_TREE_OBJECTID, &[(b_key, b_data)]);
let r = node(
ROOT,
CSUM_TREE_OBJECTID,
1,
&[(a_key, LEAF_A), (b_key, LEAF_B)],
);
(HashMap::from([(LEAF_A, a), (LEAF_B, b), (ROOT, r)]), ROOT)
}
fn reader(blocks: &HashMap<u64, Vec<u8>>) -> impl Fn(u64, &mut [u8]) -> Result<()> + '_ {
move |logical: u64, buf: &mut [u8]| -> Result<()> {
let block = blocks
.get(&logical)
.unwrap_or_else(|| panic!("no block at {logical:#x} in this fixture"));
buf.copy_from_slice(&block[..buf.len()]);
Ok(())
}
}
#[test]
fn a_range_starting_in_one_leaf_and_ending_in_the_next_gets_both_halves() {
let sectorsize = 4096u64;
let base = 1 << 20;
let (blocks, root) = two_leaf_tree(base, sectorsize);
let read = reader(&blocks);
let tree = Tree::new(geom(), &read);
let start = base + 2 * sectorsize;
let found = digests_for_range(&tree, root, 4, sectorsize, start, 4 * sectorsize).unwrap();
assert_eq!(
found.len(),
4,
"expected 2 sectors from the tail of leaf A's run and 2 from \
the head of leaf B's, got {}: {:?}",
found.len(),
found.keys().collect::<Vec<_>>()
);
assert_eq!(
found[&(base + 2 * sectorsize)],
vec![0xA2; 4],
"leaf A, sector 2"
);
assert_eq!(
found[&(base + 3 * sectorsize)],
vec![0xA3; 4],
"leaf A, sector 3"
);
assert_eq!(
found[&(base + 4 * sectorsize)],
vec![0xB0; 4],
"leaf B, sector 0"
);
assert_eq!(
found[&(base + 5 * sectorsize)],
vec![0xB1; 4],
"leaf B, sector 1"
);
}
#[test]
fn the_entire_two_leaf_tree_is_read_in_one_call() {
let sectorsize = 4096u64;
let base = 1 << 20;
let (blocks, root) = two_leaf_tree(base, sectorsize);
let read = reader(&blocks);
let tree = Tree::new(geom(), &read);
let found = digests_for_range(&tree, root, 4, sectorsize, base, 8 * sectorsize).unwrap();
assert_eq!(
found.len(),
8,
"expected all 4+4 sectors across both leaves"
);
for i in 0..4u64 {
assert_eq!(found[&(base + i * sectorsize)], vec![0xA0 + i as u8; 4]);
assert_eq!(
found[&(base + (4 + i) * sectorsize)],
vec![0xB0 + i as u8; 4]
);
}
}
}