use crate::raw;
use bytes::Buf;
use std::{collections::BTreeMap, mem};
use uuid::Uuid;
fn get_uuid(buf: &mut &[u8]) -> Uuid {
let bytes: [u8; 16] = buf[..16].try_into().unwrap();
buf.advance(16);
Uuid::from_bytes(bytes)
}
#[derive(Debug, Clone)]
pub struct Stripe {
pub devid: u64,
pub offset: u64,
pub dev_uuid: Uuid,
}
#[derive(Debug, Clone)]
pub struct ChunkMapping {
pub logical: u64,
pub length: u64,
pub stripe_len: u64,
pub chunk_type: u64,
pub num_stripes: u16,
pub sub_stripes: u16,
pub stripes: Vec<Stripe>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChunkProfile {
Single,
Dup,
Raid0,
Raid1,
Raid10,
Raid5,
Raid6,
}
impl ChunkProfile {
#[must_use]
pub fn from_chunk_type(chunk_type: u64) -> Self {
if chunk_type & u64::from(raw::BTRFS_BLOCK_GROUP_RAID0) != 0 {
Self::Raid0
} else if chunk_type & u64::from(raw::BTRFS_BLOCK_GROUP_RAID10) != 0 {
Self::Raid10
} else if chunk_type & u64::from(raw::BTRFS_BLOCK_GROUP_RAID5) != 0 {
Self::Raid5
} else if chunk_type & u64::from(raw::BTRFS_BLOCK_GROUP_RAID6) != 0 {
Self::Raid6
} else if chunk_type
& u64::from(
raw::BTRFS_BLOCK_GROUP_RAID1
| raw::BTRFS_BLOCK_GROUP_RAID1C3
| raw::BTRFS_BLOCK_GROUP_RAID1C4,
)
!= 0
{
Self::Raid1
} else if chunk_type & u64::from(raw::BTRFS_BLOCK_GROUP_DUP) != 0 {
Self::Dup
} else {
Self::Single
}
}
}
impl ChunkMapping {
#[must_use]
pub fn profile(&self) -> ChunkProfile {
ChunkProfile::from_chunk_type(self.chunk_type)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StripePlacement {
pub devid: u64,
pub physical: u64,
pub buf_offset: usize,
pub len: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WritePlan {
Plain(Vec<StripePlacement>),
Parity(ParityPlan),
}
impl WritePlan {
#[cfg(test)]
#[must_use]
pub fn unwrap_plain(self) -> Vec<StripePlacement> {
match self {
Self::Plain(p) => p,
Self::Parity(_) => panic!("plan_write returned Parity"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParityPlan {
pub stripe_len: u32,
pub rows: Vec<ParityRow>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParityRow {
pub data_columns: Vec<ParityDataColumn>,
pub parity_targets: Vec<ParityTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParityDataColumn {
pub devid: u64,
pub physical: u64,
pub overlay: Option<CallerOverlay>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallerOverlay {
pub slot_offset: u32,
pub buf_offset: usize,
pub len: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParityTarget {
pub kind: ParityKind,
pub devid: u64,
pub physical: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParityKind {
P,
Q,
}
#[derive(Debug, Default)]
pub struct ChunkTreeCache {
inner: BTreeMap<u64, ChunkMapping>,
}
impl ChunkTreeCache {
pub fn insert(&mut self, mapping: ChunkMapping) {
self.inner.insert(mapping.logical, mapping);
}
#[must_use]
pub fn lookup(&self, logical: u64) -> Option<&ChunkMapping> {
self.inner
.range(..=logical)
.next_back()
.map(|(_, mapping)| mapping)
.filter(|mapping| logical < mapping.logical + mapping.length)
}
#[must_use]
pub fn resolve(&self, logical: u64) -> Option<(u64, u64)> {
let mapping = self.lookup(logical)?;
let offset_within_chunk = logical - mapping.logical;
let stripe = &mapping.stripes[0];
Some((stripe.devid, stripe.offset + offset_within_chunk))
}
#[must_use]
pub fn resolve_all(&self, logical: u64) -> Option<Vec<(u64, u64)>> {
let mapping = self.lookup(logical)?;
let offset_within_chunk = logical - mapping.logical;
Some(
mapping
.stripes
.iter()
.map(|s| (s.devid, s.offset + offset_within_chunk))
.collect(),
)
}
#[must_use]
pub fn plan_write(&self, logical: u64, len: usize) -> Option<WritePlan> {
let mapping = self.lookup(logical)?;
match mapping.profile() {
ChunkProfile::Raid5 | ChunkProfile::Raid6 => {
plan_parity_write(mapping, logical, len).map(WritePlan::Parity)
}
_ => plan_io(mapping, logical, len, false)
.map(WritePlan::Plain),
}
}
#[must_use]
pub fn plan_read(
&self,
logical: u64,
len: usize,
) -> Option<Vec<StripePlacement>> {
let mapping = self.lookup(logical)?;
match mapping.profile() {
ChunkProfile::Raid5 | ChunkProfile::Raid6 => {
plan_parity_read(mapping, logical, len)
}
_ => plan_io(mapping, logical, len, true),
}
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &ChunkMapping> {
self.inner.values()
}
}
fn plan_io(
mapping: &ChunkMapping,
logical: u64,
len: usize,
read: bool,
) -> Option<Vec<StripePlacement>> {
if len == 0 {
return Some(Vec::new());
}
let end = logical.checked_add(len as u64)?;
if end > mapping.logical.checked_add(mapping.length)? {
return None;
}
let profile = mapping.profile();
debug_assert!(
!matches!(profile, ChunkProfile::Raid5 | ChunkProfile::Raid6),
"plan_io does not handle RAID5/RAID6; route via plan_parity_*",
);
if matches!(profile, ChunkProfile::Raid5 | ChunkProfile::Raid6) {
return None;
}
let stripe_len = mapping.stripe_len;
debug_assert!(stripe_len > 0, "chunk stripe_len must be non-zero");
let factor: u64 = match profile {
ChunkProfile::Single | ChunkProfile::Dup | ChunkProfile::Raid1 => 1,
ChunkProfile::Raid0 => u64::from(mapping.num_stripes),
ChunkProfile::Raid10 => {
let sub = u64::from(mapping.sub_stripes.max(1));
u64::from(mapping.num_stripes) / sub
}
ChunkProfile::Raid5 | ChunkProfile::Raid6 => unreachable!(),
};
debug_assert!(factor >= 1, "factor must be >= 1");
let mut placements: Vec<StripePlacement> = Vec::new();
let mut buf_offset: usize = 0;
let mut cur = logical - mapping.logical; let mut remaining = len;
let mut cols: [u16; 4] = [0; 4];
while remaining > 0 {
let stripe_nr = cur / stripe_len;
let stripe_offset = cur % stripe_len;
let row_bytes =
usize::try_from((stripe_len - stripe_offset).min(remaining as u64))
.expect("row_bytes capped by remaining (usize)");
let n_cols = fill_row_columns(profile, mapping, stripe_nr, &mut cols);
let cols_to_use = if read { &cols[..1] } else { &cols[..n_cols] };
let per_device_stripe_nr = stripe_nr / factor;
let per_device_offset =
per_device_stripe_nr * stripe_len + stripe_offset;
for &col in cols_to_use {
let stripe = &mapping.stripes[col as usize];
placements.push(StripePlacement {
devid: stripe.devid,
physical: stripe.offset + per_device_offset,
buf_offset,
len: row_bytes,
});
}
buf_offset += row_bytes;
cur += row_bytes as u64;
remaining -= row_bytes;
}
Some(placements)
}
fn fill_row_columns(
profile: ChunkProfile,
mapping: &ChunkMapping,
stripe_nr: u64,
cols: &mut [u16; 4],
) -> usize {
match profile {
ChunkProfile::Single => {
cols[0] = 0;
1
}
ChunkProfile::Dup | ChunkProfile::Raid1 => {
let n = usize::from(mapping.num_stripes);
debug_assert!(n <= cols.len(), "mirror count {n} exceeds 4");
for (i, c) in cols.iter_mut().enumerate().take(n) {
*c =
u16::try_from(i).expect("mirror count fits in u16 (max 4)");
}
n
}
ChunkProfile::Raid0 => {
let col_u64 = stripe_nr % u64::from(mapping.num_stripes);
cols[0] = u16::try_from(col_u64)
.expect("col bounded by num_stripes (u16)");
1
}
ChunkProfile::Raid10 => {
let sub = mapping.sub_stripes.max(1);
let factor = mapping.num_stripes / sub;
let group_u64 = stripe_nr % u64::from(factor);
let group = u16::try_from(group_u64)
.expect("group bounded by factor (u16)");
let base = group * sub;
let n = usize::from(sub);
for (s, c) in cols.iter_mut().enumerate().take(n) {
*c = base
+ u16::try_from(s)
.expect("sub_stripes fits in u16 (max 4)");
}
n
}
ChunkProfile::Raid5 | ChunkProfile::Raid6 => {
unreachable!()
}
}
}
fn parity_columns(num_stripes: u64, nparity: u64, phys_row: u64) -> (u16, u16) {
debug_assert!(num_stripes > nparity);
let n = num_stripes;
let q = (2 * n - 1 - (phys_row % n)) % n;
let p = if nparity == 1 {
q
} else {
(2 * n - 2 - (phys_row % n)) % n
};
(
u16::try_from(p).expect("p_col bounded by num_stripes (u16)"),
u16::try_from(q).expect("q_col bounded by num_stripes (u16)"),
)
}
fn nth_data_col(
num_stripes: u16,
nparity: u64,
p_col: u16,
q_col: u16,
data_col_in_row: u64,
) -> u16 {
let mut idx: u64 = 0;
for c in 0..num_stripes {
if c == p_col || (nparity == 2 && c == q_col) {
continue;
}
if idx == data_col_in_row {
return c;
}
idx += 1;
}
panic!("data_col_in_row {data_col_in_row} out of range")
}
#[allow(clippy::too_many_arguments)]
fn build_parity_data_columns(
mapping: &ChunkMapping,
phys_row: u64,
stripe_len: u64,
data_per_row: u64,
row_logical_start: u64,
row_a: u64,
row_b: u64,
row_buf_base: usize,
(p_col, q_col): (u16, u16),
nparity: u64,
) -> Vec<ParityDataColumn> {
let mut data_columns =
Vec::with_capacity(usize::try_from(data_per_row).unwrap_or(0));
for data_idx in 0..data_per_row {
let phys_col =
nth_data_col(mapping.num_stripes, nparity, p_col, q_col, data_idx);
let stripe = &mapping.stripes[phys_col as usize];
let physical = stripe.offset + phys_row * stripe_len;
let slot_logical_start = row_logical_start + data_idx * stripe_len;
let slot_logical_end = slot_logical_start + stripe_len;
let lo = row_a.max(slot_logical_start);
let hi = row_b.min(slot_logical_end);
let overlay = (lo < hi).then(|| {
let slot_offset = u32::try_from(lo - slot_logical_start)
.expect("slot_offset < stripe_len (u32)");
let len_bytes =
u32::try_from(hi - lo).expect("overlay len < stripe_len (u32)");
let buf_offset = row_buf_base
+ usize::try_from(lo - row_a)
.expect("overlay buf_offset capped by len");
CallerOverlay {
slot_offset,
buf_offset,
len: len_bytes,
}
});
data_columns.push(ParityDataColumn {
devid: stripe.devid,
physical,
overlay,
});
}
data_columns
}
fn build_parity_targets(
mapping: &ChunkMapping,
phys_row: u64,
stripe_len: u64,
p_col: u16,
q_col: u16,
nparity: u64,
) -> Vec<ParityTarget> {
let p_stripe = &mapping.stripes[p_col as usize];
let mut targets = vec![ParityTarget {
kind: ParityKind::P,
devid: p_stripe.devid,
physical: p_stripe.offset + phys_row * stripe_len,
}];
if nparity == 2 {
let q_stripe = &mapping.stripes[q_col as usize];
targets.push(ParityTarget {
kind: ParityKind::Q,
devid: q_stripe.devid,
physical: q_stripe.offset + phys_row * stripe_len,
});
}
targets
}
fn plan_parity_write(
mapping: &ChunkMapping,
logical: u64,
len: usize,
) -> Option<ParityPlan> {
let nparity: u64 = match mapping.profile() {
ChunkProfile::Raid5 => 1,
ChunkProfile::Raid6 => 2,
_ => unreachable!("plan_parity_write called for non-RAID5/6 profile"),
};
let n = u64::from(mapping.num_stripes);
let stripe_len = mapping.stripe_len;
debug_assert!(stripe_len > 0, "chunk stripe_len must be non-zero");
debug_assert!(n > nparity, "RAID5/6 needs more stripes than parity");
let stripe_len_u32 = u32::try_from(stripe_len).ok()?;
if len == 0 {
return Some(ParityPlan {
stripe_len: stripe_len_u32,
rows: Vec::new(),
});
}
let end = logical.checked_add(len as u64)?;
if end > mapping.logical.checked_add(mapping.length)? {
return None;
}
let data_per_row = n - nparity;
let logical_per_phys_row = data_per_row * stripe_len;
let chunk_off_start = logical - mapping.logical;
let chunk_off_end = end - mapping.logical;
let phys_row_start = chunk_off_start / logical_per_phys_row;
let phys_row_end = (chunk_off_end - 1) / logical_per_phys_row;
let mut rows = Vec::with_capacity(
usize::try_from(phys_row_end - phys_row_start + 1)
.expect("phys_row count fits in usize"),
);
for phys_row in phys_row_start..=phys_row_end {
let row_logical_start = phys_row * logical_per_phys_row;
let row_logical_end = row_logical_start + logical_per_phys_row;
let row_a = chunk_off_start.max(row_logical_start);
let row_b = chunk_off_end.min(row_logical_end);
debug_assert!(row_a < row_b, "non-empty row coverage");
let row_buf_base = usize::try_from(row_a - chunk_off_start)
.expect("row_buf_base capped by len (usize)");
let (p_col, q_col) = parity_columns(n, nparity, phys_row);
let data_columns = build_parity_data_columns(
mapping,
phys_row,
stripe_len,
data_per_row,
row_logical_start,
row_a,
row_b,
row_buf_base,
(p_col, q_col),
nparity,
);
let parity_targets = build_parity_targets(
mapping, phys_row, stripe_len, p_col, q_col, nparity,
);
rows.push(ParityRow {
data_columns,
parity_targets,
});
}
Some(ParityPlan {
stripe_len: stripe_len_u32,
rows,
})
}
fn plan_parity_read(
mapping: &ChunkMapping,
logical: u64,
len: usize,
) -> Option<Vec<StripePlacement>> {
let nparity: u64 = match mapping.profile() {
ChunkProfile::Raid5 => 1,
ChunkProfile::Raid6 => 2,
_ => unreachable!("plan_parity_read called for non-RAID5/6 profile"),
};
let n = u64::from(mapping.num_stripes);
let stripe_len = mapping.stripe_len;
debug_assert!(stripe_len > 0, "chunk stripe_len must be non-zero");
debug_assert!(n > nparity, "RAID5/6 needs more stripes than parity");
if len == 0 {
return Some(Vec::new());
}
let end = logical.checked_add(len as u64)?;
if end > mapping.logical.checked_add(mapping.length)? {
return None;
}
let data_per_row = n - nparity;
let mut placements = Vec::new();
let mut buf_offset: usize = 0;
let mut cur = logical - mapping.logical;
let mut remaining = len;
while remaining > 0 {
let stripe_nr = cur / stripe_len;
let stripe_offset = cur % stripe_len;
let row_bytes =
usize::try_from((stripe_len - stripe_offset).min(remaining as u64))
.expect("row_bytes capped by remaining (usize)");
let phys_row = stripe_nr / data_per_row;
let data_col_in_row = stripe_nr % data_per_row;
let (p_col, q_col) = parity_columns(n, nparity, phys_row);
let phys_col = nth_data_col(
mapping.num_stripes,
nparity,
p_col,
q_col,
data_col_in_row,
);
let stripe = &mapping.stripes[phys_col as usize];
let per_device_offset = phys_row * stripe_len + stripe_offset;
placements.push(StripePlacement {
devid: stripe.devid,
physical: stripe.offset + per_device_offset,
buf_offset,
len: row_bytes,
});
buf_offset += row_bytes;
cur += row_bytes as u64;
remaining -= row_bytes;
}
Some(placements)
}
#[must_use]
pub fn parse_chunk_item(
buf: &[u8],
logical: u64,
) -> Option<(ChunkMapping, usize)> {
let chunk_base_size = mem::offset_of!(raw::btrfs_chunk, stripe);
let stripe_size = mem::size_of::<raw::btrfs_stripe>();
if buf.len() < chunk_base_size {
return None;
}
let mut b = buf;
let length = b.get_u64_le();
b.advance(8); let stripe_len = b.get_u64_le();
let chunk_type = b.get_u64_le();
b.advance(12); let num_stripes = b.get_u16_le();
let sub_stripes = b.get_u16_le();
let total_size = chunk_base_size + num_stripes as usize * stripe_size;
if buf.len() < total_size {
return None;
}
let mut stripes = Vec::with_capacity(num_stripes as usize);
let mut b = &buf[chunk_base_size..];
for _ in 0..num_stripes as usize {
let devid = b.get_u64_le();
let offset = b.get_u64_le();
let dev_uuid = get_uuid(&mut b);
stripes.push(Stripe {
devid,
offset,
dev_uuid,
});
}
let mapping = ChunkMapping {
logical,
length,
stripe_len,
chunk_type,
num_stripes,
sub_stripes,
stripes,
};
Some((mapping, total_size))
}
#[must_use]
pub fn seed_from_sys_chunk_array(array: &[u8], size: u32) -> ChunkTreeCache {
let array = &array[..size as usize];
let mut cache = ChunkTreeCache::default();
let disk_key_size = mem::size_of::<raw::btrfs_disk_key>();
let mut offset = 0usize;
while offset + disk_key_size <= array.len() {
let mut b = &array[offset + 9..];
let key_offset = b.get_u64_le();
offset += disk_key_size;
if let Some((mapping, consumed)) =
parse_chunk_item(&array[offset..], key_offset)
{
cache.insert(mapping);
offset += consumed;
} else {
break;
}
}
cache
}
#[must_use]
pub fn chunk_item_bytes(mapping: &ChunkMapping, sector_size: u32) -> Vec<u8> {
use bytes::BufMut;
let chunk_base_size = mem::offset_of!(raw::btrfs_chunk, stripe);
let stripe_size = mem::size_of::<raw::btrfs_stripe>();
let total = chunk_base_size + mapping.num_stripes as usize * stripe_size;
let mut buf: Vec<u8> = Vec::with_capacity(total);
buf.put_u64_le(mapping.length);
buf.put_u64_le(u64::from(raw::BTRFS_EXTENT_TREE_OBJECTID));
buf.put_u64_le(mapping.stripe_len);
buf.put_u64_le(mapping.chunk_type);
buf.put_u32_le(sector_size); buf.put_u32_le(sector_size); buf.put_u32_le(sector_size); buf.put_u16_le(mapping.num_stripes);
buf.put_u16_le(mapping.sub_stripes);
debug_assert_eq!(buf.len(), chunk_base_size);
for stripe in &mapping.stripes {
buf.put_u64_le(stripe.devid);
buf.put_u64_le(stripe.offset);
buf.extend_from_slice(stripe.dev_uuid.as_bytes());
}
debug_assert_eq!(buf.len(), total);
buf
}
#[must_use]
pub fn sys_chunk_array_contains(
array: &[u8],
size: u32,
bg_start: u64,
) -> bool {
let array = &array[..size as usize];
let disk_key_size = mem::size_of::<raw::btrfs_disk_key>();
let mut offset = 0usize;
while offset + disk_key_size <= array.len() {
let mut b = &array[offset + 9..];
let key_offset = b.get_u64_le();
offset += disk_key_size;
if key_offset == bg_start {
return true;
}
let Some((_, consumed)) =
parse_chunk_item(&array[offset..], key_offset)
else {
return false;
};
offset += consumed;
}
false
}
pub fn sys_chunk_array_append(
array: &mut [u8],
size: &mut u32,
bg_start: u64,
chunk_bytes: &[u8],
) -> Result<u32, &'static str> {
use bytes::BufMut;
let disk_key_size = mem::size_of::<raw::btrfs_disk_key>();
let record_size = disk_key_size + chunk_bytes.len();
let cur = *size as usize;
if cur + record_size > array.len() {
return Err("sys_chunk_array overflow");
}
#[allow(clippy::cast_possible_truncation)]
let chunk_item_type = raw::BTRFS_CHUNK_ITEM_KEY as u8;
let mut header = [0u8; 17];
{
let mut w = &mut header[..];
w.put_u64_le(u64::from(raw::BTRFS_FIRST_CHUNK_TREE_OBJECTID));
w.put_u8(chunk_item_type);
w.put_u64_le(bg_start);
}
array[cur..cur + 17].copy_from_slice(&header);
array[cur + 17..cur + record_size].copy_from_slice(chunk_bytes);
let new_size = u32::try_from(cur + record_size).unwrap();
*size = new_size;
Ok(new_size)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_mapping(logical: u64, length: u64, physical: u64) -> ChunkMapping {
ChunkMapping {
logical,
length,
stripe_len: 65536,
chunk_type: 0,
num_stripes: 1,
sub_stripes: 0,
stripes: vec![Stripe {
devid: 1,
offset: physical,
dev_uuid: Uuid::nil(),
}],
}
}
fn make_multi_stripe_mapping(
logical: u64,
length: u64,
stripes: &[(u64, u64)],
) -> ChunkMapping {
ChunkMapping {
logical,
length,
stripe_len: 65536,
chunk_type: 0,
num_stripes: stripes.len() as u16,
sub_stripes: 0,
stripes: stripes
.iter()
.map(|&(devid, offset)| Stripe {
devid,
offset,
dev_uuid: Uuid::nil(),
})
.collect(),
}
}
#[test]
fn empty_cache() {
let cache = ChunkTreeCache::default();
assert!(cache.is_empty());
assert_eq!(cache.resolve(0), None);
}
#[test]
fn single_mapping() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_mapping(1000, 500, 2000));
assert_eq!(cache.len(), 1);
assert_eq!(cache.resolve(1000), Some((1, 2000)));
assert_eq!(cache.resolve(1100), Some((1, 2100)));
assert_eq!(cache.resolve(1499), Some((1, 2499)));
assert_eq!(cache.resolve(1500), None); assert_eq!(cache.resolve(999), None); }
#[test]
fn multiple_mappings() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_mapping(0, 1000, 5000));
cache.insert(make_mapping(1000, 1000, 6000));
cache.insert(make_mapping(5000, 2000, 10000));
assert_eq!(cache.resolve(0), Some((1, 5000)));
assert_eq!(cache.resolve(500), Some((1, 5500)));
assert_eq!(cache.resolve(1000), Some((1, 6000)));
assert_eq!(cache.resolve(1999), Some((1, 6999)));
assert_eq!(cache.resolve(2000), None); assert_eq!(cache.resolve(5000), Some((1, 10000)));
assert_eq!(cache.resolve(6999), Some((1, 11999)));
assert_eq!(cache.resolve(7000), None);
}
#[test]
fn lookup_returns_mapping() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_mapping(1000, 500, 2000));
let m = cache.lookup(1100).unwrap();
assert_eq!(m.logical, 1000);
assert_eq!(m.length, 500);
assert!(cache.lookup(500).is_none());
}
#[test]
fn resolve_returns_first_stripe_only() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
1000,
500,
&[(1, 2000), (2, 9000)],
));
assert_eq!(cache.resolve(1000), Some((1, 2000)));
assert_eq!(cache.resolve(1100), Some((1, 2100)));
assert_eq!(cache.resolve(1499), Some((1, 2499)));
}
#[test]
fn resolve_all_dup_returns_two_offsets_same_devid() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
1000,
500,
&[(1, 2000), (1, 50000)],
));
assert_eq!(cache.resolve_all(1000), Some(vec![(1, 2000), (1, 50000)]),);
assert_eq!(cache.resolve_all(1100), Some(vec![(1, 2100), (1, 50100)]),);
}
#[test]
fn resolve_all_raid1_returns_two_devids() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
1000,
500,
&[(1, 2000), (2, 9000)],
));
assert_eq!(cache.resolve_all(1000), Some(vec![(1, 2000), (2, 9000)]),);
assert_eq!(cache.resolve_all(1250), Some(vec![(1, 2250), (2, 9250)]),);
}
#[test]
fn resolve_all_raid1c3_returns_three_stripes() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
1000,
500,
&[(1, 2000), (2, 9000), (3, 0x10_0000)],
));
let placements = cache.resolve_all(1100).unwrap();
assert_eq!(placements.len(), 3);
assert_eq!(placements[0], (1, 2100));
assert_eq!(placements[1], (2, 9100));
assert_eq!(placements[2], (3, 0x10_0000 + 100));
}
#[test]
fn resolve_all_raid1c4_returns_four_stripes() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
1000,
500,
&[(1, 2000), (2, 9000), (3, 0x10_0000), (4, 0x20_0000)],
));
let placements = cache.resolve_all(1100).unwrap();
assert_eq!(placements.len(), 4);
assert_eq!(placements[0], (1, 2100));
assert_eq!(placements[3], (4, 0x20_0000 + 100));
}
#[test]
fn resolve_all_returns_none_outside_chunks() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
1000,
500,
&[(1, 2000), (2, 9000)],
));
assert_eq!(cache.resolve_all(999), None);
assert_eq!(cache.resolve_all(1500), None);
}
#[test]
fn resolve_all_with_non_dense_devids() {
let mut cache = ChunkTreeCache::default();
cache.insert(make_multi_stripe_mapping(
0,
1000,
&[(1, 100), (5, 999_000)],
));
assert_eq!(cache.resolve_all(42), Some(vec![(1, 142), (5, 999_042)]),);
}
#[test]
fn seed_from_empty_array() {
let array = [0u8; 2048];
let cache = seed_from_sys_chunk_array(&array, 0);
assert!(cache.is_empty());
}
#[test]
fn parse_chunk_item_basic() {
let chunk_base_size = mem::offset_of!(raw::btrfs_chunk, stripe);
let stripe_size = mem::size_of::<raw::btrfs_stripe>();
let total = chunk_base_size + stripe_size;
let mut buf = vec![0u8; total];
buf[0..8].copy_from_slice(&1000u64.to_le_bytes());
buf[8..16].copy_from_slice(&2u64.to_le_bytes());
buf[16..24].copy_from_slice(&65536u64.to_le_bytes());
buf[24..32].copy_from_slice(&1u64.to_le_bytes());
buf[44..46].copy_from_slice(&1u16.to_le_bytes());
buf[chunk_base_size..chunk_base_size + 8]
.copy_from_slice(&1u64.to_le_bytes());
buf[chunk_base_size + 8..chunk_base_size + 16]
.copy_from_slice(&5000u64.to_le_bytes());
let (mapping, consumed) = parse_chunk_item(&buf, 0).unwrap();
assert_eq!(consumed, total);
assert_eq!(mapping.logical, 0);
assert_eq!(mapping.length, 1000);
assert_eq!(mapping.num_stripes, 1);
assert_eq!(mapping.stripes[0].devid, 1);
assert_eq!(mapping.stripes[0].offset, 5000);
}
fn sample_mapping(
logical: u64,
length: u64,
physical: u64,
) -> ChunkMapping {
ChunkMapping {
logical,
length,
stripe_len: 65536,
chunk_type: u64::from(raw::BTRFS_BLOCK_GROUP_SYSTEM),
num_stripes: 1,
sub_stripes: 1,
stripes: vec![Stripe {
devid: 1,
offset: physical,
dev_uuid: Uuid::from_bytes([0xAB; 16]),
}],
}
}
#[test]
fn chunk_item_bytes_round_trips_via_parser() {
let m = sample_mapping(0x100_0000, 0x40_0000, 0x200_0000);
let bytes = chunk_item_bytes(&m, 4096);
let (parsed, consumed) = parse_chunk_item(&bytes, m.logical).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(parsed.logical, m.logical);
assert_eq!(parsed.length, m.length);
assert_eq!(parsed.stripe_len, m.stripe_len);
assert_eq!(parsed.chunk_type, m.chunk_type);
assert_eq!(parsed.num_stripes, 1);
assert_eq!(parsed.sub_stripes, 1);
assert_eq!(parsed.stripes[0].devid, 1);
assert_eq!(parsed.stripes[0].offset, 0x200_0000);
assert_eq!(parsed.stripes[0].dev_uuid, m.stripes[0].dev_uuid);
}
#[test]
fn sys_chunk_array_append_then_contains_and_seed() {
let mut buf = [0u8; 2048];
let mut size: u32 = 0;
let m1 = sample_mapping(0x100_0000, 0x40_0000, 0x200_0000);
let m2 = sample_mapping(0x500_0000, 0x40_0000, 0x600_0000);
let bytes1 = chunk_item_bytes(&m1, 4096);
sys_chunk_array_append(&mut buf, &mut size, m1.logical, &bytes1)
.unwrap();
assert!(sys_chunk_array_contains(&buf, size, m1.logical));
assert!(!sys_chunk_array_contains(&buf, size, m2.logical));
let bytes2 = chunk_item_bytes(&m2, 4096);
sys_chunk_array_append(&mut buf, &mut size, m2.logical, &bytes2)
.unwrap();
assert!(sys_chunk_array_contains(&buf, size, m2.logical));
let cache = seed_from_sys_chunk_array(&buf, size);
assert_eq!(cache.len(), 2);
assert!(cache.lookup(m1.logical).is_some());
assert!(cache.lookup(m2.logical).is_some());
}
#[test]
fn sys_chunk_array_append_overflow() {
let mut buf = [0u8; 100];
let mut size: u32 = 0;
let m = sample_mapping(0, 0x40_0000, 0);
let bytes = chunk_item_bytes(&m, 4096);
sys_chunk_array_append(&mut buf, &mut size, m.logical, &bytes).unwrap();
let m2 = sample_mapping(0x100_0000, 0x40_0000, 0x200_0000);
let bytes2 = chunk_item_bytes(&m2, 4096);
assert!(
sys_chunk_array_append(&mut buf, &mut size, m2.logical, &bytes2)
.is_err()
);
}
#[test]
fn parse_chunk_item_too_short() {
let buf = [0u8; 10];
assert!(parse_chunk_item(&buf, 0).is_none());
}
#[test]
fn profile_from_chunk_type_basic() {
use ChunkProfile::*;
assert_eq!(
ChunkProfile::from_chunk_type(u64::from(
raw::BTRFS_BLOCK_GROUP_DATA
)),
Single
);
let cases = [
(raw::BTRFS_BLOCK_GROUP_DUP, Dup),
(raw::BTRFS_BLOCK_GROUP_RAID0, Raid0),
(raw::BTRFS_BLOCK_GROUP_RAID1, Raid1),
(raw::BTRFS_BLOCK_GROUP_RAID10, Raid10),
(raw::BTRFS_BLOCK_GROUP_RAID5, Raid5),
(raw::BTRFS_BLOCK_GROUP_RAID6, Raid6),
];
for (bit, expected) in cases {
let ct = u64::from(bit) | u64::from(raw::BTRFS_BLOCK_GROUP_DATA);
assert_eq!(ChunkProfile::from_chunk_type(ct), expected);
}
}
#[test]
fn profile_from_chunk_type_raid1c3_and_c4() {
let c3 = u64::from(raw::BTRFS_BLOCK_GROUP_RAID1C3);
let c4 = u64::from(raw::BTRFS_BLOCK_GROUP_RAID1C4);
assert_eq!(ChunkProfile::from_chunk_type(c3), ChunkProfile::Raid1);
assert_eq!(ChunkProfile::from_chunk_type(c4), ChunkProfile::Raid1);
}
fn make_chunk(
chunk_type_bit: u32,
num_stripes: u16,
sub_stripes: u16,
stripe_len: u64,
length: u64,
stripes: &[(u64, u64)],
) -> ChunkMapping {
ChunkMapping {
logical: 0,
length,
stripe_len,
chunk_type: u64::from(chunk_type_bit)
| u64::from(raw::BTRFS_BLOCK_GROUP_DATA),
num_stripes,
sub_stripes,
stripes: stripes
.iter()
.map(|&(devid, offset)| Stripe {
devid,
offset,
dev_uuid: Uuid::nil(),
})
.collect(),
}
}
fn cache_with(mapping: ChunkMapping) -> ChunkTreeCache {
let mut c = ChunkTreeCache::default();
c.insert(mapping);
c
}
#[test]
fn plan_write_single_one_row() {
let m = make_chunk(0, 1, 1, 65536, 1 << 20, &[(1, 0x1000)]);
let cache = cache_with(m);
let placements = cache.plan_write(0, 4096).unwrap().unwrap_plain();
assert_eq!(
placements,
vec![StripePlacement {
devid: 1,
physical: 0x1000,
buf_offset: 0,
len: 4096,
}]
);
}
#[test]
fn plan_write_single_spans_multiple_rows() {
let m = make_chunk(0, 1, 1, 65536, 1 << 20, &[(1, 0x10000)]);
let cache = cache_with(m);
let placements = cache
.plan_write(32 * 1024, 96 * 1024)
.unwrap()
.unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10000 + 32 * 1024);
assert_eq!(placements[0].buf_offset, 0);
assert_eq!(placements[0].len, 32 * 1024);
assert_eq!(placements[1].devid, 1);
assert_eq!(placements[1].physical, 0x10000 + 65536);
assert_eq!(placements[1].buf_offset, 32 * 1024);
assert_eq!(placements[1].len, 64 * 1024);
}
#[test]
fn plan_write_dup_writes_both_copies_same_buf_slice() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_DUP,
2,
1,
65536,
1 << 20,
&[(1, 0x1000), (1, 0x2_0000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(4096, 16384).unwrap().unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x1000 + 4096);
assert_eq!(placements[0].buf_offset, 0);
assert_eq!(placements[0].len, 16384);
assert_eq!(placements[1].devid, 1);
assert_eq!(placements[1].physical, 0x2_0000 + 4096);
assert_eq!(placements[1].buf_offset, 0);
assert_eq!(placements[1].len, 16384);
}
#[test]
fn plan_write_raid1_writes_all_mirrors() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID1,
2,
1,
65536,
1 << 20,
&[(1, 0x1000), (2, 0x2000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(0, 8192).unwrap().unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[1].devid, 2);
for p in &placements {
assert_eq!(p.buf_offset, 0);
assert_eq!(p.len, 8192);
}
}
#[test]
fn plan_write_raid1c3_writes_three_mirrors() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID1C3,
3,
1,
65536,
1 << 20,
&[(1, 0x1000), (2, 0x2000), (3, 0x3000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(0, 8192).unwrap().unwrap_plain();
assert_eq!(placements.len(), 3);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[1].devid, 2);
assert_eq!(placements[2].devid, 3);
}
#[test]
fn plan_write_raid1c4_writes_four_mirrors() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID1C4,
4,
1,
65536,
1 << 20,
&[(1, 0x1000), (2, 0x2000), (3, 0x3000), (4, 0x4000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(0, 8192).unwrap().unwrap_plain();
assert_eq!(placements.len(), 4);
assert_eq!(placements[3].devid, 4);
}
#[test]
fn plan_write_raid0_routes_first_row_to_column_zero() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
2,
1,
65536,
2 << 20,
&[(1, 0x10000), (2, 0x20000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(0, 4096).unwrap().unwrap_plain();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10000);
assert_eq!(placements[0].len, 4096);
}
#[test]
fn plan_write_raid0_second_row_routes_to_second_device() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
2,
1,
65536,
2 << 20,
&[(1, 0x10000), (2, 0x20000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(65536, 4096).unwrap().unwrap_plain();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 2);
assert_eq!(placements[0].physical, 0x20000);
}
#[test]
fn plan_write_raid0_third_row_wraps_to_first_device() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
2,
1,
65536,
2 << 20,
&[(1, 0x10000), (2, 0x20000)],
);
let cache = cache_with(m);
let placements =
cache.plan_write(2 * 65536, 4096).unwrap().unwrap_plain();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10000 + 65536);
}
#[test]
fn plan_write_raid0_spans_multiple_rows_round_robins_devices() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
3,
1,
65536,
6 << 20,
&[(1, 0x10000), (2, 0x20000), (3, 0x30000)],
);
let cache = cache_with(m);
let placements =
cache.plan_write(0, 192 * 1024).unwrap().unwrap_plain();
assert_eq!(placements.len(), 3);
for (i, p) in placements.iter().enumerate() {
assert_eq!(p.devid, (i + 1) as u64);
assert_eq!(p.buf_offset, i * 65536);
assert_eq!(p.len, 65536);
}
for p in &placements {
assert_eq!(p.physical & 0xFFFF, 0);
}
}
#[test]
fn plan_write_raid0_partial_first_row_then_full_then_partial() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
2,
1,
65536,
2 << 20,
&[(1, 0x10000), (2, 0x20000)],
);
let cache = cache_with(m);
let placements = cache
.plan_write(32 * 1024, 96 * 1024)
.unwrap()
.unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10000 + 32 * 1024);
assert_eq!(placements[0].buf_offset, 0);
assert_eq!(placements[0].len, 32 * 1024);
assert_eq!(placements[1].devid, 2);
assert_eq!(placements[1].physical, 0x20000);
assert_eq!(placements[1].buf_offset, 32 * 1024);
assert_eq!(placements[1].len, 64 * 1024);
}
#[test]
fn plan_write_raid10_first_row_writes_pair_zero() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID10,
4,
2,
65536,
4 << 20,
&[(1, 0x10000), (2, 0x20000), (3, 0x30000), (4, 0x40000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(0, 4096).unwrap().unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10000);
assert_eq!(placements[1].devid, 2);
assert_eq!(placements[1].physical, 0x20000);
for p in &placements {
assert_eq!(p.buf_offset, 0);
assert_eq!(p.len, 4096);
}
}
#[test]
fn plan_write_raid10_second_row_writes_pair_one() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID10,
4,
2,
65536,
4 << 20,
&[(1, 0x10000), (2, 0x20000), (3, 0x30000), (4, 0x40000)],
);
let cache = cache_with(m);
let placements = cache.plan_write(65536, 4096).unwrap().unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 3);
assert_eq!(placements[1].devid, 4);
}
#[test]
fn plan_write_raid10_wraps_after_factor_rows() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID10,
4,
2,
65536,
4 << 20,
&[(1, 0x10000), (2, 0x20000), (3, 0x30000), (4, 0x40000)],
);
let cache = cache_with(m);
let placements =
cache.plan_write(2 * 65536, 4096).unwrap().unwrap_plain();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10000 + 65536);
assert_eq!(placements[1].devid, 2);
assert_eq!(placements[1].physical, 0x20000 + 65536);
}
#[test]
fn plan_read_picks_first_mirror_per_row() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID1,
2,
1,
65536,
1 << 20,
&[(1, 0x1000), (2, 0x2000)],
);
let cache = cache_with(m);
let placements = cache.plan_read(0, 8192).unwrap();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 1);
}
#[test]
fn plan_read_raid10_picks_first_mirror_per_row() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID10,
4,
2,
65536,
4 << 20,
&[(1, 0x10000), (2, 0x20000), (3, 0x30000), (4, 0x40000)],
);
let cache = cache_with(m);
let placements = cache.plan_read(0, 128 * 1024).unwrap();
assert_eq!(placements.len(), 2);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[1].devid, 3);
}
#[test]
fn plan_read_raid0_matches_plan_write() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
2,
1,
65536,
2 << 20,
&[(1, 0x10000), (2, 0x20000)],
);
let cache = cache_with(m);
let r = cache.plan_read(32 * 1024, 96 * 1024).unwrap();
let w = cache
.plan_write(32 * 1024, 96 * 1024)
.unwrap()
.unwrap_plain();
assert_eq!(r, w);
}
#[test]
fn plan_write_out_of_chunk_returns_none() {
let m = make_chunk(0, 1, 1, 65536, 1 << 20, &[(1, 0)]);
let cache = cache_with(m);
assert!(cache.plan_write((1 << 20) - 4096, 8192).is_none());
assert!(cache.plan_write(2 << 20, 4096).is_none());
let p = cache.plan_write(0, 0).unwrap().unwrap_plain();
assert!(p.is_empty());
}
#[test]
fn plan_write_unmapped_returns_none() {
let cache = ChunkTreeCache::default();
assert!(cache.plan_write(0, 4096).is_none());
}
#[test]
fn plan_write_raid5_returns_parity_plan() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID5,
3,
1,
65536,
3 << 20,
&[(1, 0), (2, 0), (3, 0)],
);
let cache = cache_with(m);
let plan = cache.plan_write(0, 4096).unwrap();
match plan {
WritePlan::Parity(_) => {}
WritePlan::Plain(_) => panic!("expected Parity plan for RAID5"),
}
}
fn make_parity_chunk(
chunk_type_bit: u32,
num_stripes: u16,
stripe_len: u64,
length: u64,
) -> ChunkMapping {
let stripes: Vec<(u64, u64)> = (0..num_stripes)
.map(|i| (u64::from(i) + 1, 0x10_0000 + u64::from(i) * 0x10_0000))
.collect();
make_chunk(chunk_type_bit, num_stripes, 1, stripe_len, length, &stripes)
}
#[test]
fn plan_write_raid5_three_devices_single_row() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let WritePlan::Parity(plan) = cache.plan_write(0, 16 * 1024).unwrap()
else {
panic!("expected Parity");
};
assert_eq!(plan.stripe_len, 65536);
assert_eq!(plan.rows.len(), 1);
let row = &plan.rows[0];
assert_eq!(row.data_columns.len(), 2);
assert_eq!(row.parity_targets.len(), 1);
assert_eq!(row.parity_targets[0].kind, ParityKind::P);
assert_eq!(row.parity_targets[0].devid, 3);
assert_eq!(row.data_columns[0].devid, 1);
let ov = row.data_columns[0].overlay.as_ref().unwrap();
assert_eq!(ov.slot_offset, 0);
assert_eq!(ov.buf_offset, 0);
assert_eq!(ov.len, 16 * 1024);
assert_eq!(row.data_columns[1].devid, 2);
assert!(row.data_columns[1].overlay.is_none());
}
#[test]
fn plan_write_raid5_data_column_rotation() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 4, 65536, 8 << 20);
let cache = cache_with(m);
let row_bytes = 192 * 1024;
for phys_row in 0u64..4 {
let logical = phys_row * row_bytes;
let WritePlan::Parity(plan) =
cache.plan_write(logical, 16 * 1024).unwrap()
else {
panic!("expected Parity");
};
let row = &plan.rows[0];
let expected_p_col = ((4 - 1 - phys_row) % 4) as u16;
assert_eq!(
row.parity_targets[0].devid,
u64::from(expected_p_col) + 1,
"parity row {phys_row}",
);
}
}
#[test]
fn plan_write_raid6_four_devices_single_row() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID6, 4, 65536, 4 << 20);
let cache = cache_with(m);
let WritePlan::Parity(plan) = cache.plan_write(0, 16 * 1024).unwrap()
else {
panic!("expected Parity");
};
let row = &plan.rows[0];
assert_eq!(row.data_columns.len(), 2);
assert_eq!(row.parity_targets.len(), 2);
assert_eq!(row.parity_targets[0].kind, ParityKind::P);
assert_eq!(row.parity_targets[1].kind, ParityKind::Q);
assert_eq!(row.parity_targets[0].devid, 3);
assert_eq!(row.parity_targets[1].devid, 4);
assert_eq!(row.data_columns[0].devid, 1);
assert_eq!(row.data_columns[1].devid, 2);
}
#[test]
fn plan_write_raid56_partial_row_overlay_offsets() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let WritePlan::Parity(plan) = cache.plan_write(0x4000, 0x1000).unwrap()
else {
panic!("expected Parity");
};
let row = &plan.rows[0];
let ov = row.data_columns[0].overlay.as_ref().unwrap();
assert_eq!(ov.slot_offset, 0x4000);
assert_eq!(ov.len, 0x1000);
assert_eq!(ov.buf_offset, 0);
assert!(row.data_columns[1].overlay.is_none());
}
#[test]
fn plan_write_raid5_spanning_two_data_columns_in_one_row() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let WritePlan::Parity(plan) = cache.plan_write(0, 128 * 1024).unwrap()
else {
panic!("expected Parity");
};
assert_eq!(plan.rows.len(), 1);
let row = &plan.rows[0];
for (i, dc) in row.data_columns.iter().enumerate() {
let ov = dc.overlay.as_ref().expect("data col overlay");
assert_eq!(ov.slot_offset, 0);
assert_eq!(ov.len, 65536);
assert_eq!(ov.buf_offset, i * 65536);
}
}
#[test]
fn plan_write_raid5_spans_two_physical_rows() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let WritePlan::Parity(plan) =
cache.plan_write(64 * 1024, 192 * 1024).unwrap()
else {
panic!("expected Parity");
};
assert_eq!(plan.rows.len(), 2);
let r0 = &plan.rows[0];
assert!(r0.data_columns[0].overlay.is_none());
let ov0 = r0.data_columns[1].overlay.as_ref().unwrap();
assert_eq!(ov0.slot_offset, 0);
assert_eq!(ov0.len, 65536);
assert_eq!(ov0.buf_offset, 0);
let r1 = &plan.rows[1];
assert_eq!(r1.parity_targets[0].devid, 2);
for (i, dc) in r1.data_columns.iter().enumerate() {
let ov = dc.overlay.as_ref().unwrap();
assert_eq!(ov.slot_offset, 0);
assert_eq!(ov.len, 65536);
assert_eq!(ov.buf_offset, 64 * 1024 + i * 65536);
}
}
#[test]
fn plan_read_raid5_routes_to_data_column() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let placements = cache.plan_read(0, 4096).unwrap();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10_0000);
}
#[test]
fn plan_read_raid5_second_data_column_routes_to_devid_2() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let placements = cache.plan_read(65536, 4096).unwrap();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 2);
}
#[test]
fn plan_read_raid5_advances_to_next_physical_row() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let placements = cache.plan_read(128 * 1024, 4096).unwrap();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 1);
assert_eq!(placements[0].physical, 0x10_0000 + 65536);
}
#[test]
fn plan_read_raid6_routes_skipping_two_parity_columns() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID6, 4, 65536, 4 << 20);
let cache = cache_with(m);
let placements = cache.plan_read(0, 4096).unwrap();
assert_eq!(placements.len(), 1);
assert_eq!(placements[0].devid, 1);
}
#[test]
fn plan_write_raid5_buf_offsets_cover_request_exactly() {
let m =
make_parity_chunk(raw::BTRFS_BLOCK_GROUP_RAID5, 3, 65536, 3 << 20);
let cache = cache_with(m);
let req_len = 200 * 1024;
let WritePlan::Parity(plan) =
cache.plan_write(8 * 1024, req_len).unwrap()
else {
panic!();
};
let mut overlays: Vec<&CallerOverlay> = plan
.rows
.iter()
.flat_map(|r| r.data_columns.iter())
.filter_map(|dc| dc.overlay.as_ref())
.collect();
overlays.sort_by_key(|o| o.buf_offset);
let mut next = 0usize;
for o in &overlays {
assert_eq!(o.buf_offset, next);
next += o.len as usize;
}
assert_eq!(next, req_len);
}
#[test]
fn plan_write_buf_offsets_cover_request_exactly() {
let m = make_chunk(
raw::BTRFS_BLOCK_GROUP_RAID0,
2,
1,
65536,
4 << 20,
&[(1, 0x10000), (2, 0x20000)],
);
let cache = cache_with(m);
let placements =
cache.plan_write(0, 256 * 1024).unwrap().unwrap_plain();
let total: usize = placements.iter().map(|p| p.len).sum();
assert_eq!(total, 256 * 1024);
let mut next = 0;
for p in &placements {
assert_eq!(p.buf_offset, next);
next += p.len;
}
assert_eq!(next, 256 * 1024);
}
}