use crate::{
allocation,
buffer::ITEM_SIZE,
extent_walk::{self, AllocatedExtent},
filesystem::Filesystem,
items,
path::BtrfsPath,
search::{self, SearchIntent, next_leaf},
transaction::Transaction,
};
use btrfs_disk::{
raw,
tree::{DiskKey, KeyType},
};
use std::io::{self, Read, Seek, Write};
const EXTENT_TREE_ID: u64 = 2;
const FREE_SPACE_TREE_ID: u64 = 10;
const BLOCK_GROUP_TREE_ID: u64 = 11;
const ROOT_TREE_ID: u64 = 1;
pub fn convert_to_free_space_tree<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
let fst_bit = u64::from(raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE);
let fst_valid_bit =
u64::from(raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE_VALID);
let compat_ro = fs_info.superblock.compat_ro_flags;
if compat_ro & fst_bit != 0 {
return Err(io::Error::other(
"convert_to_free_space_tree: free space tree already enabled",
));
}
if fs_info.root_bytenr(FREE_SPACE_TREE_ID).is_some() {
return Err(io::Error::other(
"convert_to_free_space_tree: stale free space tree root present (option 2: clear with rescue first)",
));
}
if compat_ro & fst_valid_bit != 0 {
return Err(io::Error::other(
"convert_to_free_space_tree: FREE_SPACE_TREE_VALID set without FREE_SPACE_TREE; refusing to touch",
));
}
if root_tree_has_v1_cache(fs_info)? {
return Err(io::Error::other(
"convert_to_free_space_tree: v1 free-space-cache present (option 2: clear with rescue first)",
));
}
trans.create_empty_tree(fs_info, FREE_SPACE_TREE_ID)?;
seed_free_space_tree(trans, fs_info)?;
fs_info.superblock.compat_ro_flags |= fst_bit | fst_valid_bit;
fs_info.superblock.cache_generation = 0;
Ok(())
}
pub fn seed_free_space_tree<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
use btrfs_disk::items::FreeSpaceInfoFlags;
if fs_info.root_bytenr(FREE_SPACE_TREE_ID).is_none() {
return Err(io::Error::other(
"seed_free_space_tree: FST root not registered (call create_empty_tree(10) first)",
));
}
let block_groups = allocation::load_block_groups(fs_info)?;
debug_assert!(
!block_groups.is_empty(),
"seed_free_space_tree: no block groups found",
);
for bg in &block_groups {
if let Some(existing) = trans.read_free_space_info(
fs_info,
FREE_SPACE_TREE_ID,
bg.start,
bg.length,
)? {
if existing.flags.contains(FreeSpaceInfoFlags::USING_BITMAPS) {
return Err(io::Error::other(format!(
"seed_free_space_tree: BG {} uses bitmap layout (unsupported in v1)",
bg.start,
)));
}
continue;
}
let mut allocated: Vec<AllocatedExtent> = Vec::new();
extent_walk::walk_block_group_extents(
fs_info,
bg.start,
bg.length,
|e| {
allocated.push(e);
Ok(())
},
)?;
let free_ranges =
extent_walk::derive_free_ranges(bg.start, bg.length, &allocated)?;
let extent_count = u32::try_from(free_ranges.len()).map_err(|_| {
io::Error::other(format!(
"seed_free_space_tree: BG {} has too many free extents to fit in u32",
bg.start,
))
})?;
let mut info_data = [0u8; 8];
info_data[0..4].copy_from_slice(&extent_count.to_le_bytes());
let info_key = DiskKey {
objectid: bg.start,
key_type: KeyType::FreeSpaceInfo,
offset: bg.length,
};
insert_in_tree(
trans,
fs_info,
FREE_SPACE_TREE_ID,
&info_key,
&info_data,
)?;
for r in &free_ranges {
debug_assert!(r.length > 0);
let key = DiskKey {
objectid: r.start,
key_type: KeyType::FreeSpaceExtent,
offset: r.length,
};
insert_in_tree(trans, fs_info, FREE_SPACE_TREE_ID, &key, &[])?;
}
}
Ok(())
}
fn insert_in_tree<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
tree_id: u64,
key: &DiskKey,
data: &[u8],
) -> io::Result<()> {
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *trans),
fs_info,
tree_id,
key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + data.len()) as u32),
true,
)?;
if found {
path.release();
return Err(io::Error::other(format!(
"insert_in_tree: duplicate key {key:?} in tree {tree_id}",
)));
}
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| io::Error::other("insert_in_tree: no leaf in path"))?;
items::insert_item(leaf, path.slots[0], key, data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
fn root_tree_has_v1_cache<R: Read + Write + Seek>(
fs_info: &mut Filesystem<R>,
) -> io::Result<bool> {
let target_oid = raw::BTRFS_FREE_SPACE_OBJECTID as u64;
let key = DiskKey {
objectid: target_oid,
key_type: KeyType::from_raw(0),
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
None,
fs_info,
ROOT_TREE_ID,
&key,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
let result = loop {
let Some(leaf) = path.nodes[0].as_ref() else {
break false;
};
let slot = path.slots[0];
if slot >= leaf.nritems() as usize {
if !next_leaf(fs_info, &mut path)? {
break false;
}
continue;
}
let k = leaf.item_key(slot);
if k.objectid == target_oid {
break true;
}
if k.objectid > target_oid {
break false;
}
path.slots[0] = slot + 1;
};
path.release();
Ok(result)
}
struct BlockGroupItemSnapshot {
key: DiskKey,
data: Vec<u8>,
}
pub fn convert_to_block_group_tree<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
let bgt_bit = u64::from(raw::BTRFS_FEATURE_COMPAT_RO_BLOCK_GROUP_TREE);
let compat_ro = fs_info.superblock.compat_ro_flags;
if compat_ro & bgt_bit != 0 {
return Err(io::Error::other(
"convert_to_block_group_tree: block group tree already enabled",
));
}
if fs_info.root_bytenr(BLOCK_GROUP_TREE_ID).is_some() {
return Err(io::Error::other(
"convert_to_block_group_tree: stale block group tree root present (refusing)",
));
}
create_block_group_tree(trans, fs_info)
}
pub fn create_block_group_tree<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
let bgt_bit = u64::from(raw::BTRFS_FEATURE_COMPAT_RO_BLOCK_GROUP_TREE);
let fst_bit = u64::from(raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE);
let fst_valid_bit =
u64::from(raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE_VALID);
let compat_ro = fs_info.superblock.compat_ro_flags;
if compat_ro & fst_bit == 0 {
return Err(io::Error::other(
"create_block_group_tree: free space tree must be enabled first (kernel requires FST for BGT)",
));
}
if compat_ro & fst_valid_bit == 0 {
return Err(io::Error::other(
"create_block_group_tree: free space tree is not marked VALID (refusing)",
));
}
let snapshots = collect_block_group_items(fs_info)?;
let mut guard = fs_info.pin_block_group_tree(EXTENT_TREE_ID);
let fs_info = guard.fs_mut();
if fs_info.root_bytenr(BLOCK_GROUP_TREE_ID).is_none() {
trans.create_empty_tree(fs_info, BLOCK_GROUP_TREE_ID)?;
}
for snap in &snapshots {
if !key_present_in_tree(fs_info, BLOCK_GROUP_TREE_ID, &snap.key)? {
insert_in_tree(
trans,
fs_info,
BLOCK_GROUP_TREE_ID,
&snap.key,
&snap.data,
)?;
}
if key_present_in_tree(fs_info, EXTENT_TREE_ID, &snap.key)? {
delete_one(trans, fs_info, EXTENT_TREE_ID, &snap.key)?;
}
}
if compat_ro & bgt_bit == 0 {
fs_info.superblock.compat_ro_flags |= bgt_bit;
}
Ok(())
}
fn key_present_in_tree<R: Read + Write + Seek>(
fs_info: &mut Filesystem<R>,
tree_id: u64,
key: &DiskKey,
) -> io::Result<bool> {
let mut path = BtrfsPath::new();
let found = search::search_slot(
None,
fs_info,
tree_id,
key,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
path.release();
Ok(found)
}
fn collect_block_group_items<R: Read + Write + Seek>(
fs_info: &mut Filesystem<R>,
) -> io::Result<Vec<BlockGroupItemSnapshot>> {
let mut out: Vec<BlockGroupItemSnapshot> = Vec::new();
let start_key = DiskKey {
objectid: 0,
key_type: KeyType::from_raw(0),
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
None,
fs_info,
EXTENT_TREE_ID,
&start_key,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
while let Some(leaf) = path.nodes[0].as_ref() {
let slot = path.slots[0];
if slot >= leaf.nritems() as usize {
if !next_leaf(fs_info, &mut path)? {
break;
}
continue;
}
let k = leaf.item_key(slot);
if k.key_type == KeyType::BlockGroupItem {
out.push(BlockGroupItemSnapshot {
key: k,
data: leaf.item_data(slot).to_vec(),
});
}
path.slots[0] = slot + 1;
}
path.release();
debug_assert!(
out.windows(2).all(|w| {
(w[0].key.objectid, w[0].key.offset)
< (w[1].key.objectid, w[1].key.offset)
}),
"collect_block_group_items: items not strictly sorted by (start, length)",
);
Ok(out)
}
fn delete_one<R: Read + Write + Seek>(
trans: &mut Transaction<R>,
fs_info: &mut Filesystem<R>,
tree_id: u64,
key: &DiskKey,
) -> io::Result<()> {
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *trans),
fs_info,
tree_id,
key,
&mut path,
SearchIntent::Delete,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"delete_one: key {key:?} not found in tree {tree_id}",
)));
}
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| io::Error::other("delete_one: no leaf in path"))?;
items::del_items(leaf, path.slots[0], 1);
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}