use crate::chunk::objectid;
use crate::error::{Error, Result};
use crate::fs::Filesystem;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rewrite {
pub old: u64,
pub new: u64,
pub owner: u64,
pub level: u8,
}
#[derive(Debug, Clone, Default)]
pub struct Plan {
pub rewrites: Vec<Rewrite>,
}
impl Plan {
pub fn released(&self) -> Vec<u64> {
self.rewrites.iter().map(|r| r.old).collect()
}
pub fn allocated(&self) -> Vec<u64> {
self.rewrites.iter().map(|r| r.new).collect()
}
pub fn trees(&self) -> BTreeSet<u64> {
self.rewrites.iter().map(|r| r.owner).collect()
}
pub fn usage_delta(&self, nodesize: u64) -> i128 {
(self.allocated().len() as i128 - self.released().len() as i128) * nodesize as i128
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DataMove {
pub old: u64,
pub old_len: u64,
pub old_ref_offset: u64,
pub new: u64,
pub len: u64,
pub file_offset: u64,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct DataWrite {
pub root: u64,
pub ino: u64,
pub moves: Vec<DataMove>,
pub time: (u64, u32),
}
impl DataWrite {
fn addresses(&self) -> Vec<u64> {
self.moves.iter().flat_map(|m| [m.old, m.new]).collect()
}
}
type BlockVisitor<'a> = &'a mut dyn FnMut(u64, &crate::btree::TreeBlock, Option<u64>);
#[derive(Debug, Clone, Copy)]
struct Placement {
parent: Option<u64>,
owner: u64,
level: u8,
}
impl Filesystem {
pub fn plan_transaction(&self, dirty: &[u64]) -> Result<Plan> {
let places = self.placements()?;
let mut touched: BTreeSet<u64> = BTreeSet::new();
let mut queue: Vec<u64> = dirty.to_vec();
while let Some(at) = queue.pop() {
if !touched.insert(at) {
continue;
}
let place = places.get(&at).ok_or_else(|| {
Error::UnsupportedFeature(format!(
"the block at {at} is not reachable from any tree, so there is nothing \
above it to rewrite"
))
})?;
match place.parent {
Some(parent) => queue.push(parent),
None if place.owner != objectid::ROOT_TREE => {
if let Some(leaf) = self.root_item_leaf(place.owner)? {
queue.push(leaf);
}
}
None => {}
}
}
let mut plan = Plan::default();
let mut taken: BTreeSet<u64> = BTreeSet::new();
for old in touched {
let place = places[&old];
let new = self.next_free_block(&taken)?;
taken.insert(new);
plan.rewrites.push(Rewrite {
old,
new,
owner: place.owner,
level: place.level,
});
}
Ok(plan)
}
fn next_free_block(&self, taken: &BTreeSet<u64>) -> Result<u64> {
let nodesize = self.sb.nodesize as u64;
let groups: Vec<_> = self
.block_groups()?
.into_iter()
.filter(|g| g.holds_metadata())
.collect();
for runs in self.free_extents_by_group(&groups)? {
for run in runs {
let mut at = run.start.next_multiple_of(nodesize);
while at + nodesize <= run.end() {
if !taken.contains(&at) && !self.on_superblock_copy(at, nodesize)? {
return Ok(at);
}
at += nodesize;
}
}
}
Err(Error::UnsupportedFeature(format!(
"no metadata block group has room for another {nodesize}-byte block; \
allocating a new block group is not implemented"
)))
}
fn root_item_bytenr(block: &crate::btree::TreeBlock, item: &crate::btree::Item) -> Option<u64> {
let data = block.item_data(item)?;
let field = data.get(root_item::BYTENR..root_item::BYTENR + 8)?;
Some(u64::from_le_bytes(field.try_into().ok()?))
}
fn root_item_leaf(&self, objectid: u64) -> Result<Option<u64>> {
let mut found = None;
self.for_each_tree_block(self.sb.root, &mut |at, block, _| {
if found.is_some() {
return;
}
let Some(items) = block.body.items() else {
return;
};
if items
.iter()
.any(|it| it.key.objectid == objectid && it.key.key_type == ROOT_ITEM_KEY)
{
found = Some(at);
}
})?;
Ok(found)
}
fn placements(&self) -> Result<BTreeMap<u64, Placement>> {
let mut out = BTreeMap::new();
let mut roots = vec![self.sb.root];
while let Some(root) = roots.pop() {
if root == 0 {
continue;
}
let mut found_roots = Vec::new();
self.for_each_tree_block(root, &mut |at, block, parent| {
let owner = block.header.owner;
let level = block.header.level;
out.insert(
at,
Placement {
parent,
owner,
level,
},
);
if owner != objectid::ROOT_TREE {
return;
}
let Some(items) = block.body.items() else {
return;
};
for item in items {
if item.key.key_type != ROOT_ITEM_KEY {
continue;
}
let Some(b) = Self::root_item_bytenr(block, item) else {
continue;
};
if b != 0 && !out.contains_key(&b) {
found_roots.push(b);
}
}
})?;
roots.extend(found_roots);
}
Ok(out)
}
fn for_each_tree_block(&self, root: u64, visit: BlockVisitor) -> Result<()> {
let reader = self.pool_reader();
let tree = reader.tree();
let mut stack = vec![(root, None)];
let mut seen = BTreeSet::new();
while let Some((at, parent)) = stack.pop() {
if at == 0 || !seen.insert(at) {
continue;
}
let Ok(block) = tree.read_block(at) else {
continue;
};
visit(at, &block, parent);
if let Some(ptrs) = block.body.key_ptrs() {
for p in ptrs {
stack.push((p.blockptr, Some(at)));
}
}
}
Ok(())
}
}
use crate::fs::{root_item, ROOT_ITEM_KEY};
impl Filesystem {
pub fn render_plan(
&self,
plan: &Plan,
generation: u64,
) -> Result<Vec<crate::commit::PlacedBlock>> {
self.render_plan_with(plan, &DataWrite::default(), generation)
}
pub(crate) fn render_plan_with(
&self,
plan: &Plan,
data: &DataWrite,
generation: u64,
) -> Result<Vec<crate::commit::PlacedBlock>> {
use crate::commit::PlacedBlock;
use crate::leaf_edit::OwnedItem;
use crate::tree_write::{build_leaf, build_node, chunk_tree_uuid_of, BlockIdentity};
if plan.trees().contains(&objectid::FREE_SPACE_TREE) {
self.refuse_free_space_straddle(plan, data)?;
}
let moved: BTreeMap<u64, u64> = plan.rewrites.iter().map(|r| (r.old, r.new)).collect();
let deltas = self.block_group_deltas(plan, data)?;
let mut used_written: BTreeSet<u64> = BTreeSet::new();
let mut applied = DataApplied::default();
let mut out = Vec::with_capacity(plan.rewrites.len());
for rewrite in &plan.rewrites {
let block = self.read_tree_block(rewrite.old)?;
let raw = block.bytes().to_vec();
let id = BlockIdentity {
bytenr: rewrite.new,
owner: rewrite.owner,
generation,
level: rewrite.level,
flags: crate::tree_write::flags_for_new_block(),
chunk_tree_uuid: chunk_tree_uuid_of(&raw),
};
let bytes = match block.body.key_ptrs() {
Some(ptrs) => {
let updated: Vec<_> = ptrs
.iter()
.map(|p| {
let mut p = *p;
if let Some(&to) = moved.get(&p.blockptr) {
p.blockptr = to;
p.generation = generation;
}
p
})
.collect();
build_node(&self.sb, id, &updated)?
}
None => {
let items = block.body.items().unwrap_or(&[]);
let mut owned: Vec<OwnedItem> = items
.iter()
.filter_map(|item| {
block.item_data(item).map(|data| OwnedItem {
key: item.key,
data: data.to_vec(),
})
})
.collect();
if rewrite.owner == objectid::EXTENT_TREE {
owned = self.apply_records(
rewrite.old,
owned,
plan,
data,
generation,
&mut applied,
)?;
apply_block_group_used(&mut owned, &deltas, &mut used_written)?;
}
if rewrite.owner == objectid::FREE_SPACE_TREE {
owned = self.apply_free_space(owned, plan, data)?;
}
if !data.moves.is_empty() && rewrite.owner == data.root {
apply_file_write(&mut owned, data, generation, &mut applied)?;
}
if rewrite.owner == objectid::ROOT_TREE {
for item in &mut owned {
if item.key.key_type != ROOT_ITEM_KEY
|| item.data.len() < root_item::LEVEL + 1
{
continue;
}
let at = u64::from_le_bytes(
item.data[root_item::BYTENR..root_item::BYTENR + 8]
.try_into()
.expect("8 bytes"),
);
if let Some(&to) = moved.get(&at) {
item.data[root_item::BYTENR..root_item::BYTENR + 8]
.copy_from_slice(&to.to_le_bytes());
item.data[root_item::GENERATION..root_item::GENERATION + 8]
.copy_from_slice(&generation.to_le_bytes());
if let Some(v2) = item
.data
.get_mut(root_item::GENERATION_V2..root_item::GENERATION_V2 + 8)
{
v2.copy_from_slice(&generation.to_le_bytes());
}
}
}
}
let borrowed: Vec<_> = owned.iter().map(|i| i.as_leaf_item()).collect();
build_leaf(&self.sb, id, &borrowed)?
}
};
out.push(PlacedBlock {
logical: rewrite.new,
bytes,
});
}
if let Some((start, delta)) = deltas.iter().find(|(g, _)| !used_written.contains(g)) {
return Err(Error::UnsupportedFeature(format!(
"the plan moves the used count of the block group at {start} by {delta} bytes, \
but the leaf holding its block group item is not one the plan rewrites"
)));
}
let n = data.moves.len();
let inode = usize::from(n > 0);
if applied.items != n || applied.inodes != inode {
return Err(Error::UnsupportedFeature(format!(
"the write moves {n} extents of inode {}, and the plan rewrote {} of its \
extent items and {} of its inode items",
data.ino, applied.items, applied.inodes
)));
}
if applied.released != n || applied.recorded != n {
return Err(Error::UnsupportedFeature(format!(
"the write moves {n} data extents, and the plan released the records of {} and \
recorded {}",
applied.released, applied.recorded
)));
}
Ok(out)
}
pub fn planned_root(&self, plan: &Plan) -> Option<u64> {
plan.rewrites
.iter()
.find(|r| r.old == self.sb.root)
.map(|r| r.new)
}
}
impl Filesystem {
pub fn plan_transaction_closed(&self, dirty: &[u64], rounds: usize) -> Result<Plan> {
self.plan_transaction_closed_with(dirty, &DataWrite::default(), rounds)
}
pub(crate) fn plan_transaction_closed_with(
&self,
dirty: &[u64],
data: &DataWrite,
rounds: usize,
) -> Result<Plan> {
let mut seed: BTreeSet<u64> = dirty.iter().copied().collect();
for _ in 0..rounds {
let list: Vec<u64> = seed.iter().copied().collect();
let plan = self.plan_transaction(&list)?;
let mut touched: Vec<u64> = plan.released();
touched.extend(plan.allocated());
touched.extend(data.addresses());
let before = seed.len();
seed.extend(self.extent_leaves_for(&touched)?);
seed.extend(self.free_space_leaves_for(&touched)?);
let groups: BTreeSet<u64> = self.block_group_deltas(&plan, data)?.into_keys().collect();
seed.extend(self.block_group_item_leaves(&groups)?.into_values());
if seed.len() == before {
return Ok(plan);
}
}
Err(Error::UnsupportedFeature(format!(
"the transaction did not settle in {rounds} rounds: recording each round's \
moves keeps dirtying extent tree leaves that were not already in it. \
Breaking that needs reservations rather than another iteration"
)))
}
fn extent_leaves_for(&self, addresses: &[u64]) -> Result<BTreeSet<u64>> {
let root = self.tree_root(objectid::EXTENT_TREE)?;
self.leaves_holding(root, addresses)
}
fn leaves_holding(&self, root: u64, addresses: &[u64]) -> Result<BTreeSet<u64>> {
if addresses.is_empty() {
return Ok(BTreeSet::new());
}
let mut leaves: Vec<(u64, u64)> = Vec::new();
self.for_each_tree_block(root, &mut |at, block, _| {
let Some(items) = block.body.items() else {
return;
};
if let Some(first) = items.first() {
leaves.push((first.key.objectid, at));
}
})?;
if leaves.is_empty() {
return Ok(BTreeSet::new());
}
leaves.sort();
let mut out = BTreeSet::new();
for a in addresses {
let idx = match leaves.binary_search_by(|(first, _)| first.cmp(a)) {
Ok(i) => i,
Err(0) => 0,
Err(i) => i - 1,
};
out.insert(leaves[idx].1);
}
Ok(out)
}
}
impl Filesystem {
fn apply_records(
&self,
leaf: u64,
items: Vec<crate::leaf_edit::OwnedItem>,
plan: &Plan,
data: &DataWrite,
generation: u64,
applied: &mut DataApplied,
) -> Result<Vec<crate::leaf_edit::OwnedItem>> {
use crate::extent_write::{record_tree_block, TreeBlockAllocation};
use crate::leaf_edit::{delete, insert, OwnedItem};
let root = self.tree_root(objectid::EXTENT_TREE)?;
let mine =
|at: u64| -> Result<bool> { Ok(self.leaves_holding(root, &[at])?.contains(&leaf)) };
let mut out = items;
for rewrite in &plan.rewrites {
if !mine(rewrite.old)? {
continue;
}
let key = TreeBlockAllocation {
bytenr: rewrite.old,
level: rewrite.level,
generation,
owner: rewrite.owner,
}
.key();
if let Some(item) = out.iter().find(|i| i.key == key) {
let refs = item
.data
.get(..8)
.map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes")));
if refs != Some(1) {
return Err(Error::UnsupportedFeature(format!(
"the tree block at {} has {} references, so another tree -- most \
likely a snapshot -- still points at it; moving it would free a \
block that tree reads, and dropping one reference is not \
implemented",
rewrite.old,
refs.map_or("an unreadable number of".to_string(), |r| r.to_string())
)));
}
}
out = delete(&out, &key)?;
}
for m in &data.moves {
if !mine(m.old)? {
continue;
}
let key = DiskKey {
objectid: m.old,
key_type: key_type::EXTENT_ITEM,
offset: m.old_len,
};
let body = out.iter().find(|i| i.key == key).map(|i| i.data.as_slice());
let expected = data_extent_body(1, None, data.root, data.ino, m.old_ref_offset);
let held = body.is_some_and(|b| {
b.len() == expected.len()
&& b[..8] == expected[..8]
&& b[extent_body::FLAGS..] == expected[extent_body::FLAGS..]
});
if !held {
return Err(Error::UnsupportedFeature(format!(
"the data extent at {} is not recorded as one reference held inline by \
inode {} at {} in tree {}, so releasing it could free bytes something \
else reads",
m.old, data.ino, m.old_ref_offset, data.root
)));
}
out = delete(&out, &key)?;
applied.released += 1;
}
for rewrite in &plan.rewrites {
if !mine(rewrite.new)? {
continue;
}
let alloc = TreeBlockAllocation {
bytenr: rewrite.new,
level: rewrite.level,
generation,
owner: rewrite.owner,
};
let (key, body) = record_tree_block(&self.sb, alloc)?;
out = insert(
self.sb.nodesize,
&out,
OwnedItem {
key,
data: body.to_vec(),
},
)?;
}
for m in &data.moves {
if !mine(m.new)? {
continue;
}
out = insert(
self.sb.nodesize,
&out,
OwnedItem {
key: DiskKey {
objectid: m.new,
key_type: key_type::EXTENT_ITEM,
offset: m.len,
},
data: data_extent_body(1, Some(generation), data.root, data.ino, m.file_offset)
.to_vec(),
},
)?;
applied.recorded += 1;
}
Ok(out)
}
}
#[derive(Debug, Default)]
struct DataApplied {
items: usize,
inodes: usize,
released: usize,
recorded: usize,
}
mod extent_body {
pub const REFS: usize = 0;
pub const GENERATION: usize = 8;
pub const FLAGS: usize = 16;
pub const REF_TYPE: usize = 24;
pub const REF_ROOT: usize = 25;
pub const REF_OBJECTID: usize = 33;
pub const REF_OFFSET: usize = 41;
pub const REF_COUNT: usize = 49;
pub const SIZE: usize = 53;
}
const EXTENT_DATA_REF: u8 = 178;
fn data_extent_body(
refs: u64,
generation: Option<u64>,
root: u64,
ino: u64,
offset: u64,
) -> [u8; extent_body::SIZE] {
use crate::extent_write::EXTENT_FLAG_DATA;
use extent_body::*;
let mut out = [0u8; SIZE];
out[REFS..REFS + 8].copy_from_slice(&refs.to_le_bytes());
out[GENERATION..GENERATION + 8].copy_from_slice(&generation.unwrap_or(0).to_le_bytes());
out[FLAGS..FLAGS + 8].copy_from_slice(&EXTENT_FLAG_DATA.to_le_bytes());
out[REF_TYPE] = EXTENT_DATA_REF;
out[REF_ROOT..REF_ROOT + 8].copy_from_slice(&root.to_le_bytes());
out[REF_OBJECTID..REF_OBJECTID + 8].copy_from_slice(&ino.to_le_bytes());
out[REF_OFFSET..REF_OFFSET + 8].copy_from_slice(&offset.to_le_bytes());
out[REF_COUNT..REF_COUNT + 4].copy_from_slice(&1u32.to_le_bytes());
out
}
fn apply_file_write(
items: &mut [crate::leaf_edit::OwnedItem],
data: &DataWrite,
generation: u64,
applied: &mut DataApplied,
) -> Result<()> {
use crate::fs::file_extent as fe;
use crate::inode::{offsets as io, INODE_ITEM_KEY, INODE_ITEM_SIZE};
let le64 = |b: &[u8], at: usize| u64::from_le_bytes(b[at..at + 8].try_into().expect("8"));
let put64 = |b: &mut [u8], at: usize, v: u64| b[at..at + 8].copy_from_slice(&v.to_le_bytes());
for item in items.iter_mut() {
if item.key.objectid != data.ino {
continue;
}
let d = &mut item.data;
match item.key.key_type {
INODE_ITEM_KEY => {
if d.len() < INODE_ITEM_SIZE {
return Err(Error::UnsupportedFeature(format!(
"inode {}'s item is {} bytes, shorter than an inode",
data.ino,
d.len()
)));
}
put64(d, io::TRANSID, generation);
let sequence = le64(d, io::SEQUENCE).wrapping_add(1);
put64(d, io::SEQUENCE, sequence);
for at in [io::CTIME, io::MTIME] {
put64(d, at, data.time.0);
d[at + 8..at + 12].copy_from_slice(&data.time.1.to_le_bytes());
}
applied.inodes += 1;
}
EXTENT_DATA_KEY => {
let Some(m) = data.moves.iter().find(|m| m.file_offset == item.key.offset) else {
continue;
};
let regular = d.len() == fe::REGULAR_SIZE
&& d[fe::TYPE] == EXTENT_REGULAR
&& d[fe::COMPRESSION] == 0
&& d[fe::ENCRYPTION] == 0
&& d[fe::OTHER_ENCODING..fe::OTHER_ENCODING + 2] == [0, 0];
if !regular
|| le64(d, fe::DISK_BYTENR) != m.old
|| le64(d, fe::NUM_BYTES) != m.len
|| item.key.offset.checked_sub(le64(d, fe::OFFSET)) != Some(m.old_ref_offset)
{
return Err(Error::UnsupportedFeature(format!(
"inode {}'s extent item at {} is not the regular extent of {} bytes at \
{} the write was planned against",
data.ino, item.key.offset, m.len, m.old
)));
}
put64(d, fe::GENERATION, generation);
put64(d, fe::RAM_BYTES, m.len);
put64(d, fe::DISK_BYTENR, m.new);
put64(d, fe::DISK_NUM_BYTES, m.len);
put64(d, fe::OFFSET, 0);
applied.items += 1;
}
_ => {}
}
}
Ok(())
}
use crate::chunk::{key_type, DiskKey};
use crate::fs::{EXTENT_DATA_KEY, EXTENT_REGULAR};
impl Filesystem {
fn block_group_deltas(&self, plan: &Plan, data: &DataWrite) -> Result<BTreeMap<u64, i128>> {
let groups = self.block_groups()?;
let group_of = |at: u64| {
groups
.iter()
.find(|g| g.contains(at))
.map(|g| g.start)
.ok_or_else(|| {
Error::UnsupportedFeature(format!(
"the block at {at} is in no block group, so there is no used count \
to move for it"
))
})
};
let nodesize = i128::from(self.sb.nodesize);
let mut out: BTreeMap<u64, i128> = BTreeMap::new();
for rewrite in &plan.rewrites {
*out.entry(group_of(rewrite.new)?).or_default() += nodesize;
*out.entry(group_of(rewrite.old)?).or_default() -= nodesize;
}
for m in &data.moves {
*out.entry(group_of(m.new)?).or_default() += i128::from(m.len);
*out.entry(group_of(m.old)?).or_default() -= i128::from(m.old_len);
}
out.retain(|_, delta| *delta != 0);
Ok(out)
}
fn block_group_item_leaves(&self, starts: &BTreeSet<u64>) -> Result<BTreeMap<u64, u64>> {
let mut out = BTreeMap::new();
if starts.is_empty() {
return Ok(out);
}
let root = self.tree_root(objectid::EXTENT_TREE)?;
self.for_each_tree_block(root, &mut |at, block, _| {
let Some(items) = block.body.items() else {
return;
};
for item in items {
if item.key.key_type == BLOCK_GROUP_ITEM_KEY && starts.contains(&item.key.objectid)
{
out.insert(item.key.objectid, at);
}
}
})?;
Ok(out)
}
}
fn apply_block_group_used(
items: &mut [crate::leaf_edit::OwnedItem],
deltas: &BTreeMap<u64, i128>,
written: &mut BTreeSet<u64>,
) -> Result<()> {
use crate::block_group::block_group_item::{SIZE, USED};
for item in items.iter_mut() {
if item.key.key_type != BLOCK_GROUP_ITEM_KEY {
continue;
}
let Some(&delta) = deltas.get(&item.key.objectid) else {
continue;
};
let start = item.key.objectid;
let whole = item.data.len() >= SIZE;
let field = item.data.get_mut(USED..USED + 8).filter(|_| whole);
let Some(field) = field else {
return Err(Error::UnsupportedFeature(format!(
"the block group item at {start} is shorter than the structure it declares"
)));
};
let used = u64::from_le_bytes((&*field).try_into().expect("8 bytes"));
let moved = i128::from(used) + delta;
if moved < 0 || moved > i128::from(item.key.offset) {
return Err(Error::UnsupportedFeature(format!(
"the block group at {start} records {used} bytes used, and the plan would move \
that by {delta} to outside its {} bytes",
item.key.offset
)));
}
field.copy_from_slice(&(moved as u64).to_le_bytes());
written.insert(start);
}
Ok(())
}
use crate::chunk::key_type::BLOCK_GROUP_ITEM as BLOCK_GROUP_ITEM_KEY;
use crate::chunk::key_type::{
FREE_SPACE_BITMAP as FREE_SPACE_BITMAP_KEY, FREE_SPACE_EXTENT as FREE_SPACE_EXTENT_KEY,
FREE_SPACE_INFO as FREE_SPACE_INFO_KEY,
};
impl Filesystem {
fn free_space_leaves_for(&self, addresses: &[u64]) -> Result<BTreeSet<u64>> {
let Ok(root) = self.tree_root(objectid::FREE_SPACE_TREE) else {
return Ok(BTreeSet::new());
};
let groups = self.block_groups()?;
let spans: Vec<(u64, u64)> = groups
.iter()
.filter(|g| addresses.iter().any(|a| g.contains(*a)))
.map(|g| (g.start, g.end()))
.collect();
if spans.is_empty() {
return Ok(BTreeSet::new());
}
let mut keys: Vec<u64> = Vec::new();
for (start, end) in &spans {
keys.push(*start);
for a in addresses.iter().filter(|a| **a >= *start && **a < *end) {
keys.push(*a);
}
}
self.leaves_holding(root, &keys)
}
fn refuse_free_space_straddle(&self, plan: &Plan, data: &DataWrite) -> Result<()> {
let Ok(root) = self.tree_root(objectid::FREE_SPACE_TREE) else {
return Ok(());
};
let moved: Vec<u64> = plan
.released()
.into_iter()
.chain(plan.allocated())
.chain(data.addresses())
.collect();
let touched: Vec<crate::block_group::BlockGroup> = self
.block_groups()?
.into_iter()
.filter(|g| moved.iter().any(|a| g.contains(*a)))
.collect();
if touched.is_empty() {
return Ok(());
}
let mut leaves: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
self.for_each_tree_block(root, &mut |at, block, _| {
let Some(items) = block.body.items() else {
return;
};
for item in items {
if !matches!(
item.key.key_type,
FREE_SPACE_INFO_KEY | FREE_SPACE_EXTENT_KEY | FREE_SPACE_BITMAP_KEY
) {
continue;
}
if let Some(group) = touched.iter().find(|g| g.contains(item.key.objectid)) {
leaves.entry(group.start).or_default().insert(at);
}
}
})?;
match leaves.iter().find(|(_, held)| held.len() > 1) {
Some((start, held)) => Err(Error::UnsupportedFeature(format!(
"the free-space records of the block group at {start} span {} leaves, and \
rewriting a group across a leaf boundary is not implemented",
held.len()
))),
None => Ok(()),
}
}
fn apply_free_space(
&self,
items: Vec<crate::leaf_edit::OwnedItem>,
plan: &Plan,
data: &DataWrite,
) -> Result<Vec<crate::leaf_edit::OwnedItem>> {
use crate::block_group::FreeExtent;
use crate::chunk::DiskKey;
use crate::leaf_edit::OwnedItem;
if items
.iter()
.any(|i| i.key.key_type == FREE_SPACE_BITMAP_KEY)
{
return Err(Error::UnsupportedFeature(
"this free-space tree records a block group as a bitmap, and rewriting \
bits is not implemented"
.to_string(),
));
}
let groups = self.block_groups()?;
let nodesize = self.sb.nodesize as u64;
let released: Vec<(u64, u64)> = plan
.released()
.into_iter()
.map(|at| (at, nodesize))
.chain(data.moves.iter().map(|m| (m.old, m.old_len)))
.collect();
let allocated: Vec<(u64, u64)> = plan
.allocated()
.into_iter()
.map(|at| (at, nodesize))
.chain(data.moves.iter().map(|m| (m.new, m.len)))
.collect();
let mut out: Vec<OwnedItem> = Vec::with_capacity(items.len());
let mut i = 0usize;
while i < items.len() {
let item = &items[i];
if item.key.key_type != FREE_SPACE_INFO_KEY {
out.push(item.clone());
i += 1;
continue;
}
let start = item.key.objectid;
let end = start + item.key.offset;
let mut j = i + 1;
while j < items.len()
&& items[j].key.key_type != FREE_SPACE_INFO_KEY
&& items[j].key.objectid < end
{
j += 1;
}
let touched = released
.iter()
.chain(allocated.iter())
.any(|(a, _)| *a >= start && *a < end);
let group = groups.iter().find(|g| g.start == start);
match (touched, group) {
(false, _) => out.extend_from_slice(&items[i..j]),
(true, None) => {
return Err(Error::UnsupportedFeature(format!(
"the free-space tree records a group at {start} that no block group \
item describes, and the plan allocates or releases inside it"
)))
}
(true, Some(group)) => {
let mut free = self.free_extents(group)?;
for &(start, len) in released.iter().filter(|(a, _)| group.contains(*a)) {
free.push(FreeExtent { start, len });
}
free.sort();
let mut runs: Vec<FreeExtent> = crate::block_group::merge_adjacent(free);
for &(at, len) in allocated.iter().filter(|(a, _)| group.contains(*a)) {
runs = runs.into_iter().flat_map(|r| carve(r, at, len)).collect();
}
runs.retain(|r| r.len > 0);
let mut info = item.clone();
if info.data.len() < crate::block_group::free_space_info::SIZE {
return Err(Error::UnsupportedFeature(
"FREE_SPACE_INFO item is shorter than the structure it declares"
.to_string(),
));
}
let count_at = crate::block_group::free_space_info::EXTENT_COUNT;
info.data[count_at..count_at + 4]
.copy_from_slice(&(runs.len() as u32).to_le_bytes());
out.push(info);
for run in runs {
out.push(OwnedItem {
key: DiskKey {
objectid: run.start,
key_type: FREE_SPACE_EXTENT_KEY,
offset: run.len,
},
data: Vec::new(),
});
}
}
}
i = j;
}
Ok(out)
}
}
fn carve(
run: crate::block_group::FreeExtent,
at: u64,
len: u64,
) -> Vec<crate::block_group::FreeExtent> {
use crate::block_group::FreeExtent;
let end = at + len;
if end <= run.start || at >= run.end() {
return vec![run];
}
let mut out = Vec::new();
if at > run.start {
out.push(FreeExtent {
start: run.start,
len: at - run.start,
});
}
if end < run.end() {
out.push(FreeExtent {
start: end,
len: run.end() - end,
});
}
out
}
#[cfg(test)]
mod needs_host {
use crate::fs::Filesystem;
use crate::superblock::SUPER_OFFSETS;
use fs_btrfs_test_support::{oracle, temp_path};
use fs_core::FileDevice;
use std::collections::BTreeSet;
use std::sync::Arc;
#[test]
fn no_tree_block_is_allocated_over_a_superblock_copy() {
let dir = std::path::PathBuf::from(temp_path!("alloc-over-super"));
std::fs::create_dir_all(&dir).unwrap();
let img = dir.join("fs.img");
std::fs::File::create(&img)
.unwrap()
.set_len(256 * 1024 * 1024)
.unwrap();
let made = oracle("mkfs.btrfs")
.args(["-q", "-f", "-s", "4096", "-n", "16384"])
.arg(&img)
.output();
assert!(
made.status.success(),
"{}",
String::from_utf8_lossy(&made.stderr)
);
let fs = Filesystem::mount(Arc::new(FileDevice::open(&img).unwrap())).unwrap();
let nodesize = fs.sb.nodesize as u64;
let mut windows = Vec::new();
for chunk in fs.map.chunks().iter().filter(|c| c.is_metadata()) {
for stripe in &chunk.stripes {
let per_stripe = chunk.length; for &off in &SUPER_OFFSETS[1..] {
if off >= stripe.offset && off < stripe.offset + per_stripe {
let row = (off - stripe.offset) / chunk.stripe_len * chunk.stripe_len;
windows.push((stripe.offset + row, chunk.stripe_len));
}
}
}
}
assert!(
!windows.is_empty(),
"the fixture no longer puts a metadata stripe across a superblock copy"
);
let mut taken = BTreeSet::new();
while let Ok(at) = fs.next_free_block(&taken) {
taken.insert(at);
for mirror in 0..fs.map.mirrors_at(at).unwrap() {
let m = fs.map.map_mirror(at, mirror).unwrap();
for &(start, len) in &windows {
assert!(
m.physical + nodesize <= start || m.physical >= start + len,
"the block at {at} has a copy at {}, inside the superblock \
window [{start}, +{len})",
m.physical
);
}
}
}
assert!(taken.len() > 1000, "allocated only {} blocks", taken.len());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_touched_group_no_block_group_item_describes_is_refused() {
use crate::chunk::DiskKey;
use crate::leaf_edit::OwnedItem;
use crate::transaction::{Plan, Rewrite};
let dir = std::path::PathBuf::from(temp_path!("fst-no-group"));
std::fs::create_dir_all(&dir).unwrap();
let img = dir.join("fs.img");
std::fs::File::create(&img)
.unwrap()
.set_len(256 * 1024 * 1024)
.unwrap();
let made = oracle("mkfs.btrfs")
.args(["-q", "-f", "-s", "4096", "-n", "16384"])
.arg(&img)
.output();
assert!(
made.status.success(),
"{}",
String::from_utf8_lossy(&made.stderr)
);
let fs = Filesystem::mount(Arc::new(FileDevice::open(&img).unwrap())).unwrap();
let nodesize = fs.sb.nodesize as u64;
let group = fs
.block_groups()
.unwrap()
.into_iter()
.find(|g| g.holds_metadata())
.expect("a metadata block group");
let start = group.start + nodesize;
assert!(
fs.block_groups().unwrap().iter().all(|g| g.start != start),
"a block group starts at {start}, so the INFO item below would describe it"
);
let items = vec![OwnedItem {
key: DiskKey {
objectid: start,
key_type: super::FREE_SPACE_INFO_KEY,
offset: group.length - nodesize,
},
data: vec![0; crate::block_group::free_space_info::SIZE],
}];
let plan = Plan {
rewrites: vec![Rewrite {
old: start + nodesize,
new: start + 2 * nodesize,
owner: crate::chunk::objectid::FS_TREE,
level: 0,
}],
};
let outcome = fs.apply_free_space(items, &plan, &super::DataWrite::default());
let _ = std::fs::remove_dir_all(&dir);
let error = outcome.expect_err(
"the plan moves blocks inside a group no block group item describes, and its \
records were carried through as if nothing inside it had changed",
);
assert!(
error.to_string().contains("no block group item describes"),
"refused, but not for the missing group: {error}"
);
}
}