use crate::{
allocation,
buffer::{ExtentBuffer, HEADER_SIZE, ITEM_SIZE},
cow::cow_block,
delayed_ref::{DelayedRefKey, DelayedRefQueue},
filesystem::Filesystem,
free_space::{BlockGroupRangeDeltas, Range},
items,
path::BtrfsPath,
search::{self, SearchIntent},
};
use btrfs_disk::{
chunk::{
chunk_item_bytes, parse_chunk_item, sys_chunk_array_append,
sys_chunk_array_contains,
},
items::{BlockGroupFlags, ExtentItem, RootItem},
tree::{DiskKey, KeyType},
};
use std::{
collections::{BTreeMap, BTreeSet},
io::{self, Read, Seek, Write},
};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum BlockGroupKind {
Metadata,
System,
Data,
}
#[derive(Debug, Clone, Copy)]
struct AllocCursor {
cursor: u64,
end: u64,
}
pub struct Transaction<R> {
pub transid: u64,
freed_blocks: Vec<u64>,
allocated_blocks: Vec<u64>,
pub delayed_refs: DelayedRefQueue,
pub bg_range_deltas: BlockGroupRangeDeltas,
alloc: BTreeMap<BlockGroupKind, AllocCursor>,
pinned: BTreeSet<u64>,
_phantom: std::marker::PhantomData<R>,
}
impl<R: Read + Write + Seek> Transaction<R> {
pub fn start(fs_info: &mut Filesystem<R>) -> io::Result<Self> {
let transid = fs_info.superblock.generation + 1;
assert!(
transid > fs_info.superblock.generation,
"start: transid {transid} did not advance beyond superblock \
generation {}",
fs_info.superblock.generation,
);
fs_info.generation = transid;
fs_info.snapshot_roots();
let nodesize = u64::from(fs_info.nodesize);
let (cursor, end) = find_alloc_region_after(
fs_info,
BlockGroupKind::Metadata,
0,
nodesize,
nodesize,
)?;
let mut alloc = BTreeMap::new();
alloc.insert(BlockGroupKind::Metadata, AllocCursor { cursor, end });
Ok(Self {
transid,
freed_blocks: Vec::new(),
allocated_blocks: Vec::new(),
delayed_refs: DelayedRefQueue::new(),
bg_range_deltas: BlockGroupRangeDeltas::new(),
alloc,
pinned: BTreeSet::new(),
_phantom: std::marker::PhantomData,
})
}
pub fn alloc_block(
&mut self,
fs_info: &mut Filesystem<R>,
kind: BlockGroupKind,
) -> io::Result<u64> {
let nodesize = u64::from(fs_info.nodesize);
#[allow(clippy::map_entry)]
if !self.alloc.contains_key(&kind) {
let (cursor, end) =
find_alloc_region_after(fs_info, kind, 0, nodesize, nodesize)?;
self.alloc.insert(kind, AllocCursor { cursor, end });
}
loop {
let mut state = *self.alloc.get(&kind).unwrap();
if state.cursor + nodesize > state.end {
let (cursor, end) = find_alloc_region_after(
fs_info,
kind,
state.cursor,
nodesize,
nodesize,
)?;
state = AllocCursor { cursor, end };
if state.cursor + nodesize > state.end {
return Err(io::Error::other(format!(
"no {kind:?} block group with enough free space",
)));
}
}
let logical = state.cursor;
state.cursor += nodesize;
self.alloc.insert(kind, state);
if self.pinned.contains(&logical) {
continue;
}
debug_assert!(
!self.pinned.contains(&logical),
"alloc_block: allocated pinned address {logical:#x}",
);
debug_assert_eq!(
logical % u64::from(fs_info.nodesize),
0,
"alloc_block: address {logical:#x} not aligned to nodesize {}",
fs_info.nodesize,
);
self.allocated_blocks.push(logical);
return Ok(logical);
}
}
pub fn alloc_tree_block(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
level: u8,
) -> io::Result<u64> {
let kind = if tree_id
== u64::from(btrfs_disk::raw::BTRFS_CHUNK_TREE_OBJECTID)
{
BlockGroupKind::System
} else {
BlockGroupKind::Metadata
};
let logical = self.alloc_block(fs_info, kind)?;
self.delayed_refs.add_ref(logical, true, tree_id, level);
if kind == BlockGroupKind::System {
self.ensure_in_sys_chunk_array(fs_info, logical)?;
}
Ok(logical)
}
pub fn alloc_data_extent(
&mut self,
fs_info: &mut Filesystem<R>,
data: &[u8],
owner_root: u64,
owner_ino: u64,
owner_offset: u64,
) -> io::Result<u64> {
let sectorsize = u64::from(fs_info.sectorsize);
let raw_len = data.len() as u64;
let aligned_size = align_up(raw_len, sectorsize);
if aligned_size == 0 {
return Err(io::Error::other(
"alloc_data_extent: empty data not supported",
));
}
let logical = self.reserve_data_extent(
fs_info,
aligned_size,
owner_root,
owner_ino,
owner_offset,
)?;
if raw_len == aligned_size {
fs_info.reader_mut().write_block(logical, data)?;
} else {
let mut padded = Vec::with_capacity(aligned_size as usize);
padded.extend_from_slice(data);
padded.resize(aligned_size as usize, 0);
fs_info.reader_mut().write_block(logical, &padded)?;
}
Ok(logical)
}
pub fn reserve_data_extent(
&mut self,
fs_info: &mut Filesystem<R>,
aligned_size: u64,
owner_root: u64,
owner_ino: u64,
owner_offset: u64,
) -> io::Result<u64> {
let sectorsize = u64::from(fs_info.sectorsize);
if aligned_size == 0 {
return Err(io::Error::other(
"reserve_data_extent: aligned_size must be > 0",
));
}
if !aligned_size.is_multiple_of(sectorsize) {
return Err(io::Error::other(format!(
"reserve_data_extent: aligned_size {aligned_size} not a \
multiple of sectorsize {sectorsize}"
)));
}
let kind = BlockGroupKind::Data;
#[allow(clippy::map_entry)]
if !self.alloc.contains_key(&kind) {
let (cursor, end) = find_alloc_region_after(
fs_info,
kind,
0,
sectorsize,
aligned_size,
)?;
self.alloc.insert(kind, AllocCursor { cursor, end });
}
let mut state = *self.alloc.get(&kind).unwrap();
if state.cursor + aligned_size > state.end {
let (cursor, end) = find_alloc_region_after(
fs_info,
kind,
state.cursor,
sectorsize,
aligned_size,
)?;
state = AllocCursor { cursor, end };
if state.cursor + aligned_size > state.end {
return Err(io::Error::other(
"no DATA block group with enough free space",
));
}
}
let logical = state.cursor;
state.cursor += aligned_size;
self.alloc.insert(kind, state);
debug_assert_eq!(
logical % sectorsize,
0,
"reserve_data_extent: address {logical:#x} not aligned to \
sectorsize {sectorsize}",
);
self.delayed_refs.add_data_ref(
logical,
aligned_size,
owner_root,
owner_ino,
owner_offset,
1,
);
self.allocated_blocks.push(logical);
Ok(logical)
}
pub fn insert_file_extent(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
file_offset: u64,
extent_data: &[u8],
) -> io::Result<()> {
let key = DiskKey {
objectid: inode,
key_type: KeyType::ExtentData,
offset: file_offset,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
tree_id,
&key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + extent_data.len()) as u32),
true,
)?;
if found {
path.release();
return Err(io::Error::other(format!(
"insert_file_extent: EXTENT_DATA already exists at \
(ino={inode}, offset={file_offset}) in tree {tree_id}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("insert_file_extent: no leaf in path")
})?;
let slot = path.slots[0];
items::insert_item(leaf, slot, &key, extent_data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
pub fn insert_csums(
&mut self,
fs_info: &mut Filesystem<R>,
logical_bytenr: u64,
on_disk_data: &[u8],
) -> io::Result<()> {
use btrfs_disk::superblock::ChecksumType;
if fs_info.superblock.csum_type != ChecksumType::Crc32 {
return Err(io::Error::other(format!(
"insert_csums: only CRC32C is supported (csum_type = {:?})",
fs_info.superblock.csum_type,
)));
}
let sectorsize = u64::from(fs_info.sectorsize);
let total = on_disk_data.len() as u64;
if total == 0 || !total.is_multiple_of(sectorsize) {
return Err(io::Error::other(format!(
"insert_csums: on_disk_data length {total} not a multiple of \
sectorsize {sectorsize}",
)));
}
let csum_size: usize = 4;
let num_sectors = (total / sectorsize) as usize;
let mut all_csums = Vec::with_capacity(num_sectors * csum_size);
for sector in on_disk_data.chunks_exact(sectorsize as usize) {
let csum = crc32c::crc32c(sector);
all_csums.extend_from_slice(&csum.to_le_bytes());
}
let leaf_data_size = (fs_info.nodesize as usize) - HEADER_SIZE;
let max_payload =
leaf_data_size.saturating_sub(2 * ITEM_SIZE) - csum_size;
let max_csums_per_item = (max_payload / csum_size).max(1);
let csum_objectid =
i64::from(btrfs_disk::raw::BTRFS_EXTENT_CSUM_OBJECTID) as u64;
let mut sector_idx = 0usize;
while sector_idx < num_sectors {
let take = (num_sectors - sector_idx).min(max_csums_per_item);
let payload_start = sector_idx * csum_size;
let payload_end = payload_start + take * csum_size;
let payload = &all_csums[payload_start..payload_end];
let chunk_logical =
logical_bytenr + (sector_idx as u64) * sectorsize;
let key = DiskKey {
objectid: csum_objectid,
key_type: KeyType::ExtentCsum,
offset: chunk_logical,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
7, &key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + payload.len()) as u32),
true,
)?;
if found {
path.release();
return Err(io::Error::other(format!(
"insert_csums: csum item already exists at {chunk_logical}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("insert_csums: no leaf in path")
})?;
let slot = path.slots[0];
items::insert_item(leaf, slot, &key, payload)?;
fs_info.mark_dirty(leaf);
path.release();
sector_idx += take;
}
Ok(())
}
pub fn update_inode_nbytes(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
delta: i64,
) -> io::Result<()> {
if delta == 0 {
return Ok(());
}
let nbytes_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, nbytes);
let key = DiskKey {
objectid: inode,
key_type: KeyType::InodeItem,
offset: 0,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
tree_id,
&key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"update_inode_nbytes: INODE_ITEM missing for inode {inode} in \
tree {tree_id}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("update_inode_nbytes: no leaf in path")
})?;
let slot = path.slots[0];
let item_len = leaf.item_size(slot) as usize;
if item_len < nbytes_off + 8 {
path.release();
return Err(io::Error::other(format!(
"update_inode_nbytes: INODE_ITEM payload {item_len} bytes < {}",
nbytes_off + 8,
)));
}
let payload = leaf.item_data_mut(slot);
let mut current = u64::from_le_bytes(
payload[nbytes_off..nbytes_off + 8].try_into().unwrap(),
);
if delta < 0 {
let abs = (-delta) as u64;
current = current.checked_sub(abs).ok_or_else(|| {
io::Error::other(format!(
"update_inode_nbytes: underflow (current {current}, delta {delta})"
))
})?;
} else {
current = current.checked_add(delta as u64).ok_or_else(|| {
io::Error::other("update_inode_nbytes: overflow")
})?;
}
payload[nbytes_off..nbytes_off + 8]
.copy_from_slice(¤t.to_le_bytes());
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
pub fn set_inode_nlink(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
nlink: u32,
) -> io::Result<()> {
let nlink_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, nlink);
let key = DiskKey {
objectid: inode,
key_type: KeyType::InodeItem,
offset: 0,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
tree_id,
&key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"set_inode_nlink: INODE_ITEM missing for inode {inode} in \
tree {tree_id}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("set_inode_nlink: no leaf in path")
})?;
let slot = path.slots[0];
let item_len = leaf.item_size(slot) as usize;
if item_len < nlink_off + 4 {
path.release();
return Err(io::Error::other(format!(
"set_inode_nlink: INODE_ITEM payload {item_len} bytes < {}",
nlink_off + 4,
)));
}
let payload = leaf.item_data_mut(slot);
payload[nlink_off..nlink_off + 4].copy_from_slice(&nlink.to_le_bytes());
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
pub fn insert_inline_extent(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
file_offset: u64,
data: &[u8],
compression: Option<btrfs_disk::items::CompressionType>,
) -> io::Result<()> {
use btrfs_disk::items::{CompressionType, FileExtentItem};
if file_offset != 0 {
return Err(io::Error::other(format!(
"insert_inline_extent: file_offset must be 0, got {file_offset}"
)));
}
if data.is_empty() {
return Err(io::Error::other(
"insert_inline_extent: empty data not supported",
));
}
let (embed, comp_byte) = match compression {
Some(ct) => match try_compress(data, ct) {
Some(c) => (c, ct),
None => (data.to_vec(), CompressionType::None),
},
None => (data.to_vec(), CompressionType::None),
};
let max = max_inline_data_size(fs_info.sectorsize, fs_info.nodesize);
if embed.len() > max {
return Err(io::Error::other(format!(
"insert_inline_extent: payload {} bytes exceeds inline limit {max}",
embed.len(),
)));
}
let extent_data = FileExtentItem::to_bytes_inline(
self.transid,
data.len() as u64,
comp_byte,
&embed,
);
self.insert_file_extent(fs_info, tree_id, inode, 0, &extent_data)?;
self.update_inode_nbytes(fs_info, tree_id, inode, data.len() as i64)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn write_file_data(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
file_offset: u64,
data: &[u8],
nodatasum: bool,
compression: Option<btrfs_disk::items::CompressionType>,
) -> io::Result<()> {
use btrfs_disk::items::{CompressionType, FileExtentItem};
const MAX_EXTENT_SIZE: usize = 1024 * 1024;
if data.is_empty() {
return Err(io::Error::other(
"write_file_data: empty data not supported",
));
}
if file_offset == 0
&& data.len()
<= max_inline_data_size(fs_info.sectorsize, fs_info.nodesize)
{
return self.insert_inline_extent(
fs_info,
tree_id,
inode,
0,
data,
compression,
);
}
let sectorsize = u64::from(fs_info.sectorsize);
let mut chunk_offset = 0usize;
while chunk_offset < data.len() {
let chunk_end = (chunk_offset + MAX_EXTENT_SIZE).min(data.len());
let chunk = &data[chunk_offset..chunk_end];
let chunk_logical_offset = file_offset + chunk_offset as u64;
let aligned_logical = align_up(chunk.len() as u64, sectorsize);
let (disk_bytes, comp_byte) = match compression {
Some(ct) => {
match try_compress_regular(chunk, ct, fs_info.sectorsize) {
Some(c) => (c, ct),
None => (chunk.to_vec(), CompressionType::None),
}
}
None => (chunk.to_vec(), CompressionType::None),
};
let aligned_disk = align_up(disk_bytes.len() as u64, sectorsize);
let logical = self.alloc_data_extent(
fs_info,
&disk_bytes,
tree_id,
inode,
chunk_logical_offset,
)?;
let extent_data = FileExtentItem::to_bytes_regular(
self.transid,
aligned_logical,
comp_byte,
false,
logical,
aligned_disk,
0,
aligned_logical,
);
self.insert_file_extent(
fs_info,
tree_id,
inode,
chunk_logical_offset,
&extent_data,
)?;
if !nodatasum {
let on_disk = fs_info
.reader_mut()
.read_data(logical, aligned_disk as usize)?;
self.insert_csums(fs_info, logical, &on_disk)?;
}
self.update_inode_nbytes(
fs_info,
tree_id,
inode,
aligned_logical as i64,
)?;
chunk_offset = chunk_end;
}
Ok(())
}
pub fn create_inode(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
args: &crate::inode::InodeArgs,
) -> io::Result<()> {
let key = DiskKey {
objectid: inode,
key_type: KeyType::InodeItem,
offset: 0,
};
let data = args.to_bytes();
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
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!(
"create_inode: INODE_ITEM already exists for inode {inode} \
in tree {tree_id}"
)));
}
let leaf = path.nodes[0]
.as_mut()
.ok_or_else(|| io::Error::other("create_inode: no leaf in path"))?;
let slot = path.slots[0];
items::insert_item(leaf, slot, &key, &data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn link_dir_entry(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
parent_inode: u64,
child_inode: u64,
name: &[u8],
file_type: u8,
dir_index: u64,
time: btrfs_disk::items::Timespec,
) -> io::Result<()> {
use btrfs_disk::{
items::{DirItem, InodeRef},
util::btrfs_name_hash,
};
let transid = self.transid;
let iref_data = InodeRef::serialize(dir_index, name);
let iref_key = DiskKey {
objectid: child_inode,
key_type: KeyType::InodeRef,
offset: parent_inode,
};
self.insert_item_helper(fs_info, tree_id, &iref_key, &iref_data)?;
let location = DiskKey {
objectid: child_inode,
key_type: KeyType::InodeItem,
offset: 0,
};
let dir_data = DirItem::serialize(&location, transid, file_type, name);
let dir_item_key = DiskKey {
objectid: parent_inode,
key_type: KeyType::DirItem,
offset: u64::from(btrfs_name_hash(name)),
};
self.insert_item_helper(fs_info, tree_id, &dir_item_key, &dir_data)?;
let dir_index_key = DiskKey {
objectid: parent_inode,
key_type: KeyType::DirIndex,
offset: dir_index,
};
self.insert_item_helper(fs_info, tree_id, &dir_index_key, &dir_data)?;
self.bump_dir_size(
fs_info,
tree_id,
parent_inode,
(name.len() as u64) * 2,
transid,
time,
)?;
if parent_inode == u64::from(btrfs_disk::raw::BTRFS_FIRST_FREE_OBJECTID)
{
self.mirror_root_item_size(
fs_info,
tree_id,
(name.len() as u64) * 2,
)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn link_subvol_entry(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
parent_inode: u64,
subvol_id: u64,
name: &[u8],
dir_index: u64,
time: btrfs_disk::items::Timespec,
) -> io::Result<()> {
use btrfs_disk::{items::DirItem, util::btrfs_name_hash};
let transid = self.transid;
let location = DiskKey {
objectid: subvol_id,
key_type: KeyType::RootItem,
offset: 0,
};
let dir_data = DirItem::serialize(
&location,
transid,
btrfs_disk::raw::BTRFS_FT_DIR as u8,
name,
);
let dir_item_key = DiskKey {
objectid: parent_inode,
key_type: KeyType::DirItem,
offset: u64::from(btrfs_name_hash(name)),
};
self.insert_item_helper(fs_info, tree_id, &dir_item_key, &dir_data)?;
let dir_index_key = DiskKey {
objectid: parent_inode,
key_type: KeyType::DirIndex,
offset: dir_index,
};
self.insert_item_helper(fs_info, tree_id, &dir_index_key, &dir_data)?;
self.bump_dir_size(
fs_info,
tree_id,
parent_inode,
(name.len() as u64) * 2,
transid,
time,
)?;
if parent_inode == u64::from(btrfs_disk::raw::BTRFS_FIRST_FREE_OBJECTID)
{
self.mirror_root_item_size(
fs_info,
tree_id,
(name.len() as u64) * 2,
)?;
}
Ok(())
}
fn mirror_root_item_size(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
delta: u64,
) -> io::Result<()> {
use btrfs_disk::items::RootItem;
let root_key = DiskKey {
objectid: tree_id,
key_type: KeyType::RootItem,
offset: 0,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
1,
&root_key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"mirror_root_item_size: ROOT_ITEM for tree {tree_id} not found"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("mirror_root_item_size: no leaf in path")
})?;
let slot = path.slots[0];
let ri_data = leaf.item_data(slot).to_vec();
let mut root_item = RootItem::parse(&ri_data).ok_or_else(|| {
io::Error::other("mirror_root_item_size: malformed ROOT_ITEM")
})?;
let size_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, size);
if root_item.inode_data.len() < size_off + 8 {
path.release();
return Err(io::Error::other(
"mirror_root_item_size: ROOT_ITEM inode_data shorter than btrfs_inode_item",
));
}
let mut size = u64::from_le_bytes(
root_item.inode_data[size_off..size_off + 8]
.try_into()
.unwrap(),
);
size += delta;
root_item.inode_data[size_off..size_off + 8]
.copy_from_slice(&size.to_le_bytes());
let new_ri = root_item.to_bytes();
items::update_item(leaf, slot, &new_ri)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
fn insert_item_helper(
&mut self,
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 *self),
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_item_helper: item already exists at {key:?} in tree {tree_id}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("insert_item_helper: no leaf in path")
})?;
let slot = path.slots[0];
items::insert_item(leaf, slot, key, data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
fn bump_dir_size(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
delta: u64,
transid: u64,
time: btrfs_disk::items::Timespec,
) -> io::Result<()> {
let key = DiskKey {
objectid: inode,
key_type: KeyType::InodeItem,
offset: 0,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
tree_id,
&key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"bump_dir_size: INODE_ITEM missing for inode {inode}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("bump_dir_size: no leaf in path")
})?;
let slot = path.slots[0];
let transid_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, transid);
let size_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, size);
let ctime_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, ctime);
let mtime_off =
std::mem::offset_of!(btrfs_disk::raw::btrfs_inode_item, mtime);
let payload = leaf.item_data_mut(slot);
let mut size = u64::from_le_bytes(
payload[size_off..size_off + 8].try_into().unwrap(),
);
size += delta;
payload[size_off..size_off + 8].copy_from_slice(&size.to_le_bytes());
payload[transid_off..transid_off + 8]
.copy_from_slice(&transid.to_le_bytes());
payload[ctime_off..ctime_off + 8]
.copy_from_slice(&time.sec.to_le_bytes());
payload[ctime_off + 8..ctime_off + 12]
.copy_from_slice(&time.nsec.to_le_bytes());
payload[mtime_off..mtime_off + 8]
.copy_from_slice(&time.sec.to_le_bytes());
payload[mtime_off + 8..mtime_off + 12]
.copy_from_slice(&time.nsec.to_le_bytes());
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
pub fn set_xattr(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
inode: u64,
name: &[u8],
value: &[u8],
) -> io::Result<()> {
use btrfs_disk::util::btrfs_name_hash;
use bytes::BufMut;
let total = 17 + 8 + 2 + 2 + 1 + name.len() + value.len();
let mut data = Vec::with_capacity(total);
data.extend_from_slice(&[0u8; 17]);
data.put_u64_le(self.transid);
data.put_u16_le(value.len() as u16);
data.put_u16_le(name.len() as u16);
data.put_u8(btrfs_disk::raw::BTRFS_FT_XATTR as u8);
data.put_slice(name);
data.put_slice(value);
debug_assert_eq!(data.len(), total);
let key = DiskKey {
objectid: inode,
key_type: KeyType::XattrItem,
offset: u64::from(btrfs_name_hash(name)),
};
self.insert_item_helper(fs_info, tree_id, &key, &data)
}
pub fn insert_root_ref(
&mut self,
fs_info: &mut Filesystem<R>,
parent_root: u64,
child_root: u64,
dirid: u64,
dir_index: u64,
name: &[u8],
) -> io::Result<()> {
use btrfs_disk::items::RootRef;
let payload = RootRef::serialize(dirid, dir_index, name);
let root_tree_id = 1u64;
let ref_key = DiskKey {
objectid: parent_root,
key_type: KeyType::RootRef,
offset: child_root,
};
self.insert_item_helper(fs_info, root_tree_id, &ref_key, &payload)?;
let backref_key = DiskKey {
objectid: child_root,
key_type: KeyType::RootBackref,
offset: parent_root,
};
self.insert_item_helper(fs_info, root_tree_id, &backref_key, &payload)?;
Ok(())
}
pub fn set_root_readonly(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
) -> io::Result<()> {
use btrfs_disk::items::{RootItem, RootItemFlags};
let key = DiskKey {
objectid: tree_id,
key_type: KeyType::RootItem,
offset: 0,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
1, &key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"set_root_readonly: ROOT_ITEM for tree {tree_id} not found"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("set_root_readonly: no leaf in path")
})?;
let slot = path.slots[0];
let ri_data = leaf.item_data(slot).to_vec();
let mut root_item = RootItem::parse(&ri_data).ok_or_else(|| {
io::Error::other("set_root_readonly: malformed ROOT_ITEM")
})?;
root_item.flags |= RootItemFlags::RDONLY;
let new_ri = root_item.to_bytes();
items::update_item(leaf, slot, &new_ri)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
pub fn set_default_subvol(
&mut self,
fs_info: &mut Filesystem<R>,
subvol_id: u64,
) -> io::Result<()> {
use btrfs_disk::{items::DirItem, util::btrfs_name_hash};
const DEFAULT_NAME: &[u8] = b"default";
let location = DiskKey {
objectid: subvol_id,
key_type: KeyType::RootItem,
offset: u64::MAX,
};
let payload = DirItem::serialize(
&location,
self.transid,
btrfs_disk::raw::BTRFS_FT_DIR as u8,
DEFAULT_NAME,
);
let key = DiskKey {
objectid: u64::from(btrfs_disk::raw::BTRFS_ROOT_TREE_DIR_OBJECTID),
key_type: KeyType::DirItem,
offset: u64::from(btrfs_name_hash(DEFAULT_NAME)),
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
1,
&key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + payload.len()) as u32),
true,
)?;
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("set_default_subvol: no leaf in path")
})?;
let slot = path.slots[0];
if found {
items::update_item(leaf, slot, &payload)?;
} else {
items::insert_item(leaf, slot, &key, &payload)?;
}
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
pub fn set_device_total_bytes(
&mut self,
fs_info: &mut Filesystem<R>,
devid: u64,
new_total: u64,
) -> io::Result<u64> {
const TOTAL_BYTES_OFFSET: usize = 8;
let key = DiskKey {
objectid: u64::from(btrfs_disk::raw::BTRFS_DEV_ITEMS_OBJECTID),
key_type: KeyType::DeviceItem,
offset: devid,
};
let chunk_tree = u64::from(btrfs_disk::raw::BTRFS_CHUNK_TREE_OBJECTID);
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
chunk_tree,
&key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"set_device_total_bytes: DEV_ITEM for devid {devid} not found"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("set_device_total_bytes: no leaf in path")
})?;
let slot = path.slots[0];
let item_len = leaf.item_size(slot) as usize;
if item_len < TOTAL_BYTES_OFFSET + 8 {
path.release();
return Err(io::Error::other(format!(
"set_device_total_bytes: DEV_ITEM payload {item_len} bytes < {}",
TOTAL_BYTES_OFFSET + 8,
)));
}
let payload = leaf.item_data_mut(slot);
let old_total = u64::from_le_bytes(
payload[TOTAL_BYTES_OFFSET..TOTAL_BYTES_OFFSET + 8]
.try_into()
.unwrap(),
);
payload[TOTAL_BYTES_OFFSET..TOTAL_BYTES_OFFSET + 8]
.copy_from_slice(&new_total.to_le_bytes());
fs_info.mark_dirty(leaf);
path.release();
if fs_info.superblock.dev_item.devid == devid {
fs_info.superblock.dev_item.total_bytes = new_total;
}
Ok(old_total)
}
pub fn pin_block(&mut self, logical: u64) {
self.pinned.insert(logical);
}
#[must_use]
pub fn is_pinned(&self, logical: u64) -> bool {
self.pinned.contains(&logical)
}
pub fn queue_free_block(&mut self, logical: u64) {
self.freed_blocks.push(logical);
}
pub fn create_empty_tree(
&mut self,
fs_info: &mut Filesystem<R>,
tree_id: u64,
) -> io::Result<u64> {
if matches!(tree_id, 0..=3) {
return Err(io::Error::other(format!(
"create_empty_tree: tree id {tree_id} is reserved bootstrap state",
)));
}
if fs_info.root_bytenr(tree_id).is_some() {
return Err(io::Error::other(format!(
"create_empty_tree: tree id {tree_id} already exists",
)));
}
let (fsid, chunk_tree_uuid) = {
let root_bytenr = fs_info.root_bytenr(1).ok_or_else(|| {
io::Error::other(
"create_empty_tree: root tree (id 1) has no root bytenr",
)
})?;
let eb = fs_info.read_block(root_bytenr)?;
(eb.fsid(), eb.chunk_tree_uuid())
};
let new_logical = self.alloc_tree_block(fs_info, tree_id, 0)?;
let nodesize = fs_info.nodesize;
let mut new_eb = ExtentBuffer::new_zeroed(nodesize, new_logical);
new_eb.set_bytenr(new_logical);
new_eb.set_level(0);
new_eb.set_nritems(0);
new_eb.set_generation(self.transid);
new_eb.set_owner(tree_id);
new_eb.set_fsid(&fsid);
new_eb.set_chunk_tree_uuid(&chunk_tree_uuid);
new_eb.set_flags(
u64::from(btrfs_disk::raw::BTRFS_MIXED_BACKREF_REV)
<< btrfs_disk::raw::BTRFS_BACKREF_REV_SHIFT,
);
debug_assert_eq!(new_eb.level(), 0);
debug_assert_eq!(new_eb.nritems(), 0);
debug_assert_eq!(new_eb.owner(), tree_id);
debug_assert_eq!(new_eb.generation(), self.transid);
debug_assert_eq!(
new_eb.leaf_free_space(),
nodesize - HEADER_SIZE as u32,
"create_empty_tree: empty leaf must have full free space",
);
fs_info.mark_dirty(&new_eb);
fs_info.set_root_bytenr(tree_id, new_logical);
let root_item = RootItem::new_internal(self.transid, new_logical, 0);
let root_item_bytes = root_item.to_bytes();
let root_item_key = DiskKey {
objectid: tree_id,
key_type: KeyType::RootItem,
offset: 0,
};
let root_tree_id = 1u64;
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
root_tree_id,
&root_item_key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + root_item_bytes.len()) as u32),
true,
)?;
if found {
path.release();
return Err(io::Error::other(format!(
"create_empty_tree: ROOT_ITEM for tree {tree_id} already in root tree",
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("create_empty_tree: no leaf in path after search")
})?;
items::insert_item(
leaf,
path.slots[0],
&root_item_key,
&root_item_bytes,
)?;
fs_info.mark_dirty(leaf);
path.release();
debug_assert_eq!(
fs_info.root_bytenr(tree_id),
Some(new_logical),
"create_empty_tree: roots map not updated",
);
Ok(new_logical)
}
pub fn rebuild_chunk_tree(
&mut self,
fs_info: &mut Filesystem<R>,
system_chunks: &[(u64, Vec<u8>)],
) -> io::Result<u64> {
fs_info.superblock.sys_chunk_array_size = 0;
fs_info.superblock.sys_chunk_array = [0; 2048];
for (bg_start, chunk_bytes) in system_chunks {
sys_chunk_array_append(
&mut fs_info.superblock.sys_chunk_array,
&mut fs_info.superblock.sys_chunk_array_size,
*bg_start,
chunk_bytes,
)
.map_err(io::Error::other)?;
}
let chunk_tree_id =
u64::from(btrfs_disk::raw::BTRFS_CHUNK_TREE_OBJECTID);
let new_logical = self.alloc_tree_block(fs_info, chunk_tree_id, 0)?;
let (fsid, chunk_tree_uuid) = {
let root_bytenr = fs_info.root_bytenr(1).ok_or_else(|| {
io::Error::other("rebuild_chunk_tree: root tree has no root")
})?;
let eb = fs_info.read_block(root_bytenr)?;
(eb.fsid(), eb.chunk_tree_uuid())
};
let nodesize = fs_info.nodesize;
let mut new_eb = ExtentBuffer::new_zeroed(nodesize, new_logical);
new_eb.set_bytenr(new_logical);
new_eb.set_level(0);
new_eb.set_nritems(0);
new_eb.set_generation(self.transid);
new_eb.set_owner(chunk_tree_id);
new_eb.set_fsid(&fsid);
new_eb.set_chunk_tree_uuid(&chunk_tree_uuid);
new_eb.set_flags(
u64::from(btrfs_disk::raw::BTRFS_MIXED_BACKREF_REV)
<< btrfs_disk::raw::BTRFS_BACKREF_REV_SHIFT,
);
fs_info.mark_dirty(&new_eb);
fs_info.set_root_bytenr(chunk_tree_id, new_logical);
Ok(new_logical)
}
pub fn commit(mut self, fs_info: &mut Filesystem<R>) -> io::Result<()> {
let root_tree_id = 1u64;
if let Some(root_bytenr) = fs_info.root_bytenr(root_tree_id) {
let eb = fs_info.read_block(root_bytenr)?;
let new_eb =
cow_block(&mut self, fs_info, &eb, root_tree_id, None)?;
if new_eb.logical() != root_bytenr {
fs_info.set_root_bytenr(root_tree_id, new_eb.logical());
}
}
let max_passes = 32;
for pass in 0..max_passes {
self.flush_delayed_refs(fs_info)?;
self.update_root_items(fs_info)?;
fs_info.snapshot_roots();
let fst_changed = self.update_free_space_tree(fs_info)?;
if self.delayed_refs.is_empty()
&& fs_info.changed_roots().is_empty()
&& self.bg_range_deltas.is_empty()
&& !fst_changed
{
break;
}
if pass == max_passes - 1 {
return Err(io::Error::other(
"commit convergence loop did not stabilize",
));
}
}
fs_info.flush_dirty()?;
fs_info.superblock.generation = self.transid;
if let Some(root_bytenr) = fs_info.root_bytenr(1) {
fs_info.superblock.root = root_bytenr;
if let Ok(eb) = fs_info.read_block(root_bytenr) {
fs_info.superblock.root_level = eb.level();
}
}
if let Some(chunk_bytenr) = fs_info.root_bytenr(3)
&& chunk_bytenr != fs_info.superblock.chunk_root
{
fs_info.superblock.chunk_root = chunk_bytenr;
fs_info.superblock.chunk_root_generation = self.transid;
if let Ok(eb) = fs_info.read_block(chunk_bytenr) {
fs_info.superblock.chunk_root_level = eb.level();
}
}
assert_eq!(
fs_info.superblock.generation, self.transid,
"commit: superblock generation {} != transid {}",
fs_info.superblock.generation, self.transid,
);
assert_eq!(
fs_info.superblock.root,
fs_info.root_bytenr(1).unwrap_or(0),
"commit: superblock.root doesn't match in-memory root tree root",
);
let min_bytes_used = 6 * u64::from(fs_info.nodesize);
assert!(
fs_info.superblock.bytes_used >= min_bytes_used,
"commit: bytes_used {} below kernel minimum {} \
(6 * nodesize {})",
fs_info.superblock.bytes_used,
min_bytes_used,
fs_info.nodesize,
);
assert!(
self.delayed_refs.is_empty(),
"commit: {} delayed refs still pending at superblock write",
self.delayed_refs.len(),
);
let backup_idx = (self.transid % 4) as usize;
update_backup_root(fs_info, backup_idx);
fs_info.write_superblock_all_devices()?;
for dev in fs_info.reader_mut().devices_mut().values_mut() {
dev.flush()?;
}
self.bg_range_deltas.clear();
fs_info.clear_dirty();
fs_info.clear_cache();
Ok(())
}
fn update_root_items(
&mut self,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
let changed = fs_info.changed_roots();
if changed.is_empty() {
return Ok(());
}
let root_tree_id = 1u64;
for (tree_id, new_bytenr, new_level) in changed {
let key = DiskKey {
objectid: tree_id,
key_type: KeyType::RootItem,
offset: 0,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
root_tree_id,
&key,
&mut path,
SearchIntent::ReadOnly,
true, )?;
if !found {
path.release();
continue;
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("update_root_items: no leaf in path")
})?;
let slot = path.slots[0];
let item_data = leaf.item_data(slot).to_vec();
if let Some(mut root_item) = RootItem::parse(&item_data) {
root_item.bytenr = new_bytenr;
root_item.generation = self.transid;
root_item.generation_v2 = self.transid;
root_item.level = new_level;
let new_data = root_item.to_bytes();
if new_data.len() == item_data.len() {
items::update_item(leaf, slot, &new_data)?;
fs_info.mark_dirty(leaf);
} else {
items::del_items(leaf, slot, 1);
fs_info.mark_dirty(leaf);
path.release();
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
root_tree_id,
&key,
&mut path,
SearchIntent::Insert(
(ITEM_SIZE + new_data.len()) as u32,
),
true,
)?;
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other(
"update_root_items: no leaf after reinsert search",
)
})?;
items::insert_item(leaf, path.slots[0], &key, &new_data)?;
fs_info.mark_dirty(leaf);
path.release();
continue;
}
}
path.release();
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn flush_delayed_refs(
&mut self,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
let skinny = fs_info.superblock.incompat_flags
& u64::from(
btrfs_disk::raw::BTRFS_FEATURE_INCOMPAT_SKINNY_METADATA,
)
!= 0;
let extent_tree_id = 2u64;
let nodesize = i64::from(fs_info.nodesize);
let block_groups = allocation::load_block_groups(fs_info)?;
let mut bg_deltas: BTreeMap<u64, i64> = BTreeMap::new();
let mut bytes_used_delta: i64 = 0;
let max_iterations = 32;
for iteration in 0..max_iterations {
let refs = self.delayed_refs.drain();
if refs.is_empty() {
break;
}
for dref in refs {
match dref.key {
DelayedRefKey::Metadata {
bytenr,
owner_root,
level,
} => {
if dref.delta > 0 {
self.create_metadata_extent(
fs_info,
extent_tree_id,
bytenr,
level,
owner_root,
skinny,
)?;
bytes_used_delta += nodesize;
if let Some(bg_start) = find_containing_block_group(
&block_groups,
bytenr,
) {
*bg_deltas.entry(bg_start).or_insert(0) +=
nodesize;
self.bg_range_deltas.record_allocated(
bg_start,
Range::new(bytenr, nodesize as u64),
);
}
} else if dref.delta < 0 {
self.delete_metadata_extent(
fs_info,
extent_tree_id,
bytenr,
level,
skinny,
)?;
bytes_used_delta -= nodesize;
if let Some(bg_start) = find_containing_block_group(
&block_groups,
bytenr,
) {
*bg_deltas.entry(bg_start).or_insert(0) -=
nodesize;
self.bg_range_deltas.record_freed(
bg_start,
Range::new(bytenr, nodesize as u64),
);
}
}
}
DelayedRefKey::Data {
bytenr,
owner_root,
owner_ino,
owner_offset,
} => {
let num_bytes = dref.num_bytes;
if num_bytes == 0 {
return Err(io::Error::other(
"data delayed ref missing num_bytes",
));
}
if dref.delta > 0 {
let count = dref.delta as u32;
self.create_data_extent(
fs_info,
extent_tree_id,
bytenr,
num_bytes,
owner_root,
owner_ino,
owner_offset,
count,
)?;
let signed = num_bytes as i64;
bytes_used_delta += signed;
if let Some(bg_start) = find_containing_block_group(
&block_groups,
bytenr,
) {
*bg_deltas.entry(bg_start).or_insert(0) +=
signed;
self.bg_range_deltas.record_allocated(
bg_start,
Range::new(bytenr, num_bytes),
);
}
} else if dref.delta < 0 {
let refs_to_drop = (-dref.delta) as u32;
let new_total_refs = self.drop_data_extent_ref(
fs_info,
extent_tree_id,
bytenr,
num_bytes,
owner_root,
owner_ino,
owner_offset,
refs_to_drop,
)?;
if new_total_refs == 0 {
self.delete_data_extent_item(
fs_info,
extent_tree_id,
bytenr,
num_bytes,
)?;
self.delete_csums_in_range(
fs_info, bytenr, num_bytes,
)?;
let signed = num_bytes as i64;
bytes_used_delta -= signed;
if let Some(bg_start) =
find_containing_block_group(
&block_groups,
bytenr,
)
{
*bg_deltas.entry(bg_start).or_insert(0) -=
signed;
self.bg_range_deltas.record_freed(
bg_start,
Range::new(bytenr, num_bytes),
);
}
}
}
}
}
}
if iteration == max_iterations - 1 && !self.delayed_refs.is_empty()
{
return Err(io::Error::other(
"delayed ref flush did not converge after 32 iterations",
));
}
}
self.bg_range_deltas.cancel_within_transaction();
if bytes_used_delta != 0 {
let current = fs_info.superblock.bytes_used as i64;
fs_info.superblock.bytes_used = (current + bytes_used_delta) as u64;
}
for (bg_start, delta) in &bg_deltas {
if *delta != 0 {
self.update_block_group_used(fs_info, *bg_start, *delta)?;
}
}
Ok(())
}
fn update_free_space_tree(
&mut self,
fs_info: &mut Filesystem<R>,
) -> io::Result<bool> {
use crate::free_space::{Range, apply_delta};
use btrfs_disk::items::FreeSpaceInfoFlags;
let fst_id = 10u64;
let fst_flag =
u64::from(btrfs_disk::raw::BTRFS_FEATURE_COMPAT_RO_FREE_SPACE_TREE);
if fs_info.superblock.compat_ro_flags & fst_flag == 0
|| fs_info.root_bytenr(fst_id).is_none()
{
self.bg_range_deltas.clear();
return Ok(false);
}
let deltas = std::mem::take(&mut self.bg_range_deltas);
if deltas.is_empty() {
return Ok(false);
}
let block_groups = allocation::load_block_groups(fs_info)?;
let bg_len = |start: u64| -> Option<u64> {
block_groups
.iter()
.find(|bg| bg.start == start)
.map(|bg| bg.length)
};
let mut any_changes = false;
for (bg_start, delta) in deltas.iter() {
let bg_start = *bg_start;
let bg_length = bg_len(bg_start).ok_or_else(|| {
io::Error::other(format!(
"free space tree update: block group {bg_start} not found"
))
})?;
let bg = Range::new(bg_start, bg_length);
let info = self
.read_free_space_info(fs_info, fst_id, bg_start, bg_length)?
.ok_or_else(|| {
io::Error::other(format!(
"free space tree update: FREE_SPACE_INFO missing for block group {bg_start}"
))
})?;
if info.flags.contains(FreeSpaceInfoFlags::USING_BITMAPS) {
return Err(io::Error::other(format!(
"free space tree block group {bg_start} uses bitmap layout (unsupported in v1)"
)));
}
let existing = self.read_free_space_extents(
fs_info, fst_id, bg_start, bg_length,
)?;
let new = apply_delta(bg_start, bg, &existing, delta)
.map_err(|e| io::Error::other(e.to_string()))?;
if new == existing {
continue;
}
self.delete_free_space_extents_in_range(
fs_info, fst_id, bg_start, bg_length,
)?;
for r in new.as_slice() {
self.insert_free_space_extent(
fs_info, fst_id, r.start, r.length,
)?;
}
self.update_free_space_info_count(
fs_info,
fst_id,
bg_start,
bg_length,
u32::try_from(new.len()).unwrap_or(u32::MAX),
info.flags,
)?;
any_changes = true;
}
Ok(any_changes)
}
pub(crate) fn read_free_space_info(
&mut self,
fs_info: &mut Filesystem<R>,
fst_id: u64,
bg_start: u64,
bg_length: u64,
) -> io::Result<Option<btrfs_disk::items::FreeSpaceInfo>> {
use btrfs_disk::items::FreeSpaceInfo;
let key = DiskKey {
objectid: bg_start,
key_type: KeyType::FreeSpaceInfo,
offset: bg_length,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&key,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
if !found {
path.release();
return Ok(None);
}
let leaf = path.nodes[0].as_ref().ok_or_else(|| {
io::Error::other("read_free_space_info: no leaf in path")
})?;
let slot = path.slots[0];
let data = leaf.item_data(slot).to_vec();
path.release();
Ok(FreeSpaceInfo::parse(&data))
}
fn read_free_space_extents(
&mut self,
fs_info: &mut Filesystem<R>,
fst_id: u64,
bg_start: u64,
bg_length: u64,
) -> io::Result<crate::free_space::RangeList> {
use crate::free_space::{Range, RangeList};
let bg_end = bg_start + bg_length;
let mut out: Vec<Range> = Vec::new();
let key = DiskKey {
objectid: bg_start,
key_type: KeyType::FreeSpaceExtent,
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&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 !search::next_leaf(fs_info, &mut path)? {
break;
}
continue;
}
let k = leaf.item_key(slot);
if k.objectid >= bg_end {
break;
}
if k.key_type == KeyType::FreeSpaceExtent && k.offset > 0 {
out.push(Range::new(k.objectid, k.offset));
}
path.slots[0] = slot + 1;
}
path.release();
let mut list = RangeList::new();
for r in out {
list.insert(r);
}
Ok(list)
}
fn delete_free_space_extents_in_range(
&mut self,
fs_info: &mut Filesystem<R>,
fst_id: u64,
bg_start: u64,
bg_length: u64,
) -> io::Result<()> {
let bg_end = bg_start + bg_length;
loop {
let key = DiskKey {
objectid: bg_start,
key_type: KeyType::FreeSpaceExtent,
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&key,
&mut path,
SearchIntent::Delete,
true,
)?;
let Some(leaf) = path.nodes[0].as_mut() else {
path.release();
break;
};
let slot = path.slots[0];
if slot >= leaf.nritems() as usize {
path.release();
break;
}
let k = leaf.item_key(slot);
if k.key_type != KeyType::FreeSpaceExtent || k.objectid >= bg_end {
path.release();
break;
}
items::del_items(leaf, slot, 1);
fs_info.mark_dirty(leaf);
path.release();
}
Ok(())
}
fn insert_free_space_extent(
&mut self,
fs_info: &mut Filesystem<R>,
fst_id: u64,
start: u64,
length: u64,
) -> io::Result<()> {
let key = DiskKey {
objectid: start,
key_type: KeyType::FreeSpaceExtent,
offset: length,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&key,
&mut path,
SearchIntent::Insert(ITEM_SIZE as u32),
true,
)?;
if found {
path.release();
return Ok(());
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("insert_free_space_extent: no leaf in path")
})?;
let slot = path.slots[0];
items::insert_item(leaf, slot, &key, &[])?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
fn update_free_space_info_count(
&mut self,
fs_info: &mut Filesystem<R>,
fst_id: u64,
bg_start: u64,
bg_length: u64,
new_count: u32,
flags: btrfs_disk::items::FreeSpaceInfoFlags,
) -> io::Result<()> {
let key = DiskKey {
objectid: bg_start,
key_type: KeyType::FreeSpaceInfo,
offset: bg_length,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"update_free_space_info_count: FREE_SPACE_INFO missing for {bg_start}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("update_free_space_info_count: no leaf in path")
})?;
let slot = path.slots[0];
let mut data = Vec::with_capacity(8);
data.extend_from_slice(&new_count.to_le_bytes());
data.extend_from_slice(&flags.bits().to_le_bytes());
items::update_item(leaf, slot, &data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
fn update_block_group_used(
&mut self,
fs_info: &mut Filesystem<R>,
bg_start: u64,
bytes_delta: i64,
) -> io::Result<()> {
use btrfs_disk::items::BlockGroupItem;
let bg_tree_id = fs_info.block_group_tree_id();
let search_key = DiskKey {
objectid: bg_start,
key_type: KeyType::BlockGroupItem,
offset: 0,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
bg_tree_id,
&search_key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
let Some(leaf) = path.nodes[0].as_mut() else {
return Ok(());
};
let slot = path.slots[0];
if slot >= leaf.nritems() as usize {
path.release();
return Ok(());
}
let item_key = leaf.item_key(slot);
if item_key.key_type != KeyType::BlockGroupItem
|| item_key.objectid != bg_start
{
path.release();
return Ok(());
}
let data = leaf.item_data(slot).to_vec();
if let Some(bg) = BlockGroupItem::parse(&data) {
let new_used = (bg.used as i64 + bytes_delta).max(0) as u64;
let new_data = BlockGroupItem {
used: new_used,
chunk_objectid: bg.chunk_objectid,
flags: bg.flags,
}
.to_bytes();
items::update_item(leaf, slot, &new_data)?;
fs_info.mark_dirty(leaf);
}
path.release();
Ok(())
}
fn create_metadata_extent(
&mut self,
fs_info: &mut Filesystem<R>,
extent_tree_id: u64,
bytenr: u64,
level: u8,
owner: u64,
skinny: bool,
) -> io::Result<()> {
let key = if skinny {
DiskKey {
objectid: bytenr,
key_type: KeyType::MetadataItem,
offset: u64::from(level),
}
} else {
DiskKey {
objectid: bytenr,
key_type: KeyType::ExtentItem,
offset: u64::from(fs_info.nodesize),
}
};
let data = if skinny {
ExtentItem::to_bytes_skinny(1, self.transid, owner)
} else {
let first_key = if let Ok(eb) = fs_info.read_block(bytenr) {
if eb.level() == 0 && eb.nritems() > 0 {
eb.item_key(0)
} else if eb.level() > 0 && eb.nritems() > 0 {
eb.key_ptr_key(0)
} else {
DiskKey {
objectid: 0,
key_type: KeyType::Unknown(0),
offset: 0,
}
}
} else {
DiskKey {
objectid: 0,
key_type: KeyType::Unknown(0),
offset: 0,
}
};
ExtentItem::to_bytes_non_skinny(
1,
self.transid,
owner,
&first_key,
level,
)
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
extent_tree_id,
&key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + data.len()) as u32),
true,
)?;
if found {
path.release();
return Ok(());
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("create_metadata_extent: no leaf in path")
})?;
let slot = path.slots[0];
items::insert_item(leaf, slot, &key, &data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn create_data_extent(
&mut self,
fs_info: &mut Filesystem<R>,
extent_tree_id: u64,
bytenr: u64,
num_bytes: u64,
owner_root: u64,
owner_ino: u64,
owner_offset: u64,
count: u32,
) -> io::Result<()> {
let key = DiskKey {
objectid: bytenr,
key_type: KeyType::ExtentItem,
offset: num_bytes,
};
let data = ExtentItem::to_bytes_data(
u64::from(count),
self.transid,
owner_root,
owner_ino,
owner_offset,
count,
);
debug_assert_eq!(data.len(), ExtentItem::DATA_INLINE_SIZE);
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
extent_tree_id,
&key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + data.len()) as u32),
true,
)?;
if found {
debug_assert!(
false,
"create_data_extent: extent item already exists at {bytenr}"
);
path.release();
return Ok(());
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("create_data_extent: no leaf in path")
})?;
let slot = path.slots[0];
items::insert_item(leaf, slot, &key, &data)?;
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
fn delete_metadata_extent(
&mut self,
fs_info: &mut Filesystem<R>,
extent_tree_id: u64,
bytenr: u64,
level: u8,
skinny: bool,
) -> io::Result<()> {
let key = if skinny {
DiskKey {
objectid: bytenr,
key_type: KeyType::MetadataItem,
offset: u64::from(level),
}
} else {
DiskKey {
objectid: bytenr,
key_type: KeyType::ExtentItem,
offset: u64::from(fs_info.nodesize),
}
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
extent_tree_id,
&key,
&mut path,
SearchIntent::Delete,
true,
)?;
if !found {
path.release();
return Ok(());
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("delete_metadata_extent: no leaf in path")
})?;
let slot = path.slots[0];
items::del_items(leaf, slot, 1);
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn drop_data_extent_ref(
&mut self,
fs_info: &mut Filesystem<R>,
extent_tree_id: u64,
bytenr: u64,
num_bytes: u64,
target_root: u64,
target_ino: u64,
target_offset: u64,
refs_to_drop: u32,
) -> io::Result<u64> {
let key = DiskKey {
objectid: bytenr,
key_type: KeyType::ExtentItem,
offset: num_bytes,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
extent_tree_id,
&key,
&mut path,
SearchIntent::Delete,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"drop_data_extent_ref: EXTENT_ITEM not found at bytenr {bytenr} num_bytes {num_bytes}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("drop_data_extent_ref: no leaf in path")
})?;
let slot = path.slots[0];
let location = locate_inline_data_ref(
leaf,
slot,
target_root,
target_ino,
target_offset,
)?;
let new_total_refs = if let Some(loc) = location {
let result =
decrement_inline_data_ref(leaf, slot, &loc, refs_to_drop)?;
fs_info.mark_dirty(leaf);
result
} else {
let item_data = leaf.item_data(slot);
if item_data.len() < 24 {
return Err(io::Error::other(
"drop_data_extent_ref: EXTENT_ITEM payload too short",
));
}
let mut current_refs =
u64::from_le_bytes(item_data[0..8].try_into().unwrap());
if u64::from(refs_to_drop) > current_refs {
return Err(io::Error::other(
"drop_data_extent_ref: EXTENT_ITEM.refs underflow",
));
}
current_refs -= u64::from(refs_to_drop);
leaf.item_data_mut(slot)[0..8]
.copy_from_slice(¤t_refs.to_le_bytes());
fs_info.mark_dirty(leaf);
path.release();
self.drop_standalone_data_ref(
fs_info,
extent_tree_id,
bytenr,
target_root,
target_ino,
target_offset,
refs_to_drop,
)?;
return Ok(current_refs);
};
path.release();
Ok(new_total_refs)
}
#[allow(clippy::too_many_arguments)]
fn drop_standalone_data_ref(
&mut self,
fs_info: &mut Filesystem<R>,
extent_tree_id: u64,
bytenr: u64,
target_root: u64,
target_ino: u64,
target_offset: u64,
refs_to_drop: u32,
) -> io::Result<()> {
use btrfs_disk::items::{ExtentDataRef, extent_data_ref_hash};
let hash = extent_data_ref_hash(target_root, target_ino, target_offset);
let key = DiskKey {
objectid: bytenr,
key_type: KeyType::ExtentDataRef,
offset: hash,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
extent_tree_id,
&key,
&mut path,
SearchIntent::Delete,
true,
)?;
loop {
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("drop_standalone_data_ref: no leaf in path")
})?;
let nritems = leaf.nritems() as usize;
if path.slots[0] >= nritems {
if !search::next_leaf(fs_info, &mut path)? {
path.release();
return Err(io::Error::other(
"drop_standalone_data_ref: ran out of leaves",
));
}
continue;
}
let slot = path.slots[0];
let item_key = leaf.item_key(slot);
if item_key.objectid != bytenr
|| item_key.key_type != KeyType::ExtentDataRef
{
path.release();
return Err(io::Error::other(format!(
"drop_standalone_data_ref: triple ({target_root},{target_ino},{target_offset}) not found at bytenr {bytenr}"
)));
}
let payload = leaf.item_data(slot).to_vec();
let parsed = ExtentDataRef::parse(&payload).ok_or_else(|| {
io::Error::other(
"drop_standalone_data_ref: malformed EXTENT_DATA_REF",
)
})?;
if parsed.root == target_root
&& parsed.objectid == target_ino
&& parsed.offset == target_offset
{
if refs_to_drop > parsed.count {
return Err(io::Error::other(
"drop_standalone_data_ref: count underflow",
));
}
let new_count = parsed.count - refs_to_drop;
if new_count == 0 {
items::del_items(leaf, slot, 1);
} else {
let mut new_payload = payload.clone();
new_payload[24..28]
.copy_from_slice(&new_count.to_le_bytes());
items::update_item(leaf, slot, &new_payload)?;
}
fs_info.mark_dirty(leaf);
path.release();
return Ok(());
}
path.slots[0] = slot + 1;
}
}
fn delete_data_extent_item(
&mut self,
fs_info: &mut Filesystem<R>,
extent_tree_id: u64,
bytenr: u64,
num_bytes: u64,
) -> io::Result<()> {
let key = DiskKey {
objectid: bytenr,
key_type: KeyType::ExtentItem,
offset: num_bytes,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
extent_tree_id,
&key,
&mut path,
SearchIntent::Delete,
true,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"delete_data_extent_item: EXTENT_ITEM missing at {bytenr}"
)));
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("delete_data_extent_item: no leaf in path")
})?;
let slot = path.slots[0];
items::del_items(leaf, slot, 1);
fs_info.mark_dirty(leaf);
path.release();
Ok(())
}
#[allow(clippy::too_many_lines, clippy::items_after_statements)]
fn delete_csums_in_range(
&mut self,
fs_info: &mut Filesystem<R>,
bytenr: u64,
num_bytes: u64,
) -> io::Result<()> {
let csum_tree_id = 7u64;
if fs_info.root_bytenr(csum_tree_id).is_none() {
return Ok(());
}
struct Surviving {
key: DiskKey,
payload: Vec<u8>,
}
struct CsumOp {
old_key: DiskKey,
survivors: Vec<Surviving>,
}
let csum_objectid =
i64::from(btrfs_disk::raw::BTRFS_EXTENT_CSUM_OBJECTID) as u64;
let sectorsize = u64::from(fs_info.superblock.sectorsize);
let csum_size: u64 = 4;
let end = bytenr + num_bytes;
let mut ops: Vec<CsumOp> = Vec::new();
{
let start_key = DiskKey {
objectid: csum_objectid,
key_type: KeyType::ExtentCsum,
offset: bytenr,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
csum_tree_id,
&start_key,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
if !found && path.slots[0] > 0 {
path.slots[0] -= 1;
}
'walk: while let Some(leaf) = path.nodes[0].as_ref() {
let nritems = leaf.nritems() as usize;
if path.slots[0] >= nritems {
if !search::next_leaf(fs_info, &mut path)? {
break;
}
continue;
}
let slot = path.slots[0];
let item_key = leaf.item_key(slot);
if item_key.objectid != csum_objectid
|| item_key.key_type != KeyType::ExtentCsum
{
if ops.is_empty() {
path.slots[0] = slot + 1;
continue;
}
break 'walk;
}
let item_size = u64::from(leaf.item_size(slot));
let csum_start = item_key.offset;
let sectors = item_size / csum_size;
let csum_end = csum_start + sectors * sectorsize;
if csum_end <= bytenr {
path.slots[0] = slot + 1;
continue;
}
if csum_start >= end {
break 'walk;
}
let payload = leaf.item_data(slot).to_vec();
let mut survivors: Vec<Surviving> = Vec::new();
if csum_start < bytenr {
let head_sectors =
((bytenr - csum_start) / sectorsize) as usize;
let head_bytes = head_sectors * csum_size as usize;
survivors.push(Surviving {
key: DiskKey {
objectid: csum_objectid,
key_type: KeyType::ExtentCsum,
offset: csum_start,
},
payload: payload[..head_bytes].to_vec(),
});
}
if csum_end > end {
let skipped_sectors =
((end - csum_start) / sectorsize) as usize;
let tail_start_bytes = skipped_sectors * csum_size as usize;
let tail_byte_count = (sectors as usize - skipped_sectors)
* csum_size as usize;
survivors.push(Surviving {
key: DiskKey {
objectid: csum_objectid,
key_type: KeyType::ExtentCsum,
offset: end,
},
payload: payload[tail_start_bytes
..tail_start_bytes + tail_byte_count]
.to_vec(),
});
}
ops.push(CsumOp {
old_key: item_key,
survivors,
});
path.slots[0] = slot + 1;
}
path.release();
}
for op in ops {
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
csum_tree_id,
&op.old_key,
&mut path,
SearchIntent::Delete,
true,
)?;
if found {
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other("delete_csums_in_range: no leaf in path")
})?;
items::del_items(leaf, path.slots[0], 1);
fs_info.mark_dirty(leaf);
}
path.release();
for sv in op.survivors {
if sv.payload.is_empty() {
continue;
}
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
csum_tree_id,
&sv.key,
&mut path,
SearchIntent::Insert((ITEM_SIZE + sv.payload.len()) as u32),
true,
)?;
if found {
path.release();
continue;
}
let leaf = path.nodes[0].as_mut().ok_or_else(|| {
io::Error::other(
"delete_csums_in_range: no leaf for insert",
)
})?;
items::insert_item(leaf, path.slots[0], &sv.key, &sv.payload)?;
fs_info.mark_dirty(leaf);
path.release();
}
}
Ok(())
}
fn ensure_in_sys_chunk_array(
&mut self,
fs_info: &mut Filesystem<R>,
logical: u64,
) -> io::Result<()> {
let _ = self;
let groups = allocation::load_block_groups(fs_info)?;
let bg = groups
.iter()
.find(|g| {
g.flags.contains(BlockGroupFlags::SYSTEM)
&& logical >= g.start
&& logical < g.start + g.length
})
.ok_or_else(|| {
io::Error::other(format!(
"ensure_in_sys_chunk_array: no SYSTEM block group contains {logical}"
))
})?;
let bg_start = bg.start;
if sys_chunk_array_contains(
&fs_info.superblock.sys_chunk_array,
fs_info.superblock.sys_chunk_array_size,
bg_start,
) {
return Ok(());
}
let key = DiskKey {
objectid: u64::from(
btrfs_disk::raw::BTRFS_FIRST_CHUNK_TREE_OBJECTID,
),
key_type: KeyType::ChunkItem,
offset: bg_start,
};
let chunk_tree_id =
u64::from(btrfs_disk::raw::BTRFS_CHUNK_TREE_OBJECTID);
let mut path = BtrfsPath::new();
let found = search::search_slot(
None,
fs_info,
chunk_tree_id,
&key,
&mut path,
SearchIntent::ReadOnly,
false,
)?;
if !found {
path.release();
return Err(io::Error::other(format!(
"ensure_in_sys_chunk_array: CHUNK_ITEM missing for bg {bg_start}"
)));
}
let leaf = path.nodes[0].as_ref().ok_or_else(|| {
io::Error::other("ensure_in_sys_chunk_array: no leaf in path")
})?;
let item_data = leaf.item_data(path.slots[0]).to_vec();
path.release();
let (mapping, _) =
parse_chunk_item(&item_data, bg_start).ok_or_else(|| {
io::Error::other(
"ensure_in_sys_chunk_array: malformed CHUNK_ITEM",
)
})?;
let chunk_bytes =
chunk_item_bytes(&mapping, fs_info.superblock.sectorsize);
let new_size = sys_chunk_array_append(
&mut fs_info.superblock.sys_chunk_array,
&mut fs_info.superblock.sys_chunk_array_size,
bg_start,
&chunk_bytes,
)
.map_err(|e| {
io::Error::other(format!("ensure_in_sys_chunk_array: {e}"))
})?;
debug_assert!(new_size > 0);
Ok(())
}
#[allow(dead_code)]
fn rebuild_free_space_tree(
&mut self,
fs_info: &mut Filesystem<R>,
) -> io::Result<()> {
use crate::allocation;
use btrfs_disk::items::FreeSpaceInfo;
let fst_id = 10u64;
if fs_info.root_bytenr(fst_id).is_none() {
return Ok(());
}
let groups = allocation::load_block_groups(fs_info)?;
for bg in &groups {
let free_ranges =
allocation::find_free_extents(fs_info, bg.start, bg.length, 1)?;
self.delete_free_space_extents(
fs_info, fst_id, bg.start, bg.length,
)?;
for &(start, len) in &free_ranges {
let key = DiskKey {
objectid: start,
key_type: KeyType::FreeSpaceExtent,
offset: len,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&key,
&mut path,
SearchIntent::Insert(ITEM_SIZE as u32),
true,
)?;
if !found {
let leaf = path.nodes[0].as_mut().unwrap();
items::insert_item(leaf, path.slots[0], &key, &[])?;
fs_info.mark_dirty(leaf);
}
path.release();
}
let info_key = DiskKey {
objectid: bg.start,
key_type: KeyType::FreeSpaceInfo,
offset: bg.length,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&info_key,
&mut path,
SearchIntent::ReadOnly,
true,
)?;
if found {
let leaf = path.nodes[0].as_mut().unwrap();
let slot = path.slots[0];
let data = leaf.item_data(slot).to_vec();
if let Some(info) = FreeSpaceInfo::parse(&data) {
let mut new_data = Vec::with_capacity(8);
new_data.extend_from_slice(
&(free_ranges.len() as u32).to_le_bytes(),
);
new_data
.extend_from_slice(&info.flags.bits().to_le_bytes());
items::update_item(leaf, slot, &new_data)?;
fs_info.mark_dirty(leaf);
}
}
path.release();
}
Ok(())
}
#[allow(dead_code)]
fn delete_free_space_extents(
&mut self,
fs_info: &mut Filesystem<R>,
fst_id: u64,
bg_start: u64,
bg_length: u64,
) -> io::Result<()> {
let bg_end = bg_start + bg_length;
let search_key = DiskKey {
objectid: bg_start,
key_type: KeyType::FreeSpaceExtent,
offset: 0,
};
loop {
let mut path = BtrfsPath::new();
let _found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&search_key,
&mut path,
SearchIntent::Delete,
true,
)?;
let Some(leaf) = path.nodes[0].as_mut() else {
break;
};
let slot = path.slots[0];
if slot >= leaf.nritems() as usize {
path.release();
break;
}
let key = leaf.item_key(slot);
if key.key_type != KeyType::FreeSpaceExtent
|| key.objectid >= bg_end
{
path.release();
break;
}
items::del_items(leaf, slot, 1);
fs_info.mark_dirty(leaf);
path.release();
}
Ok(())
}
#[allow(dead_code)]
fn update_free_space_tree_for(
&mut self,
fs_info: &mut Filesystem<R>,
allocated: &[u64],
) -> io::Result<()> {
let fst_id = 10u64;
if fs_info.root_bytenr(fst_id).is_none() {
return Ok(()); }
let nodesize = u64::from(fs_info.nodesize);
for &addr in allocated {
let search_key = DiskKey {
objectid: addr,
key_type: KeyType::FreeSpaceExtent,
offset: u64::MAX,
};
let mut path = BtrfsPath::new();
let found = search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&search_key,
&mut path,
SearchIntent::Delete,
true,
)?;
if !found && path.slots[0] > 0 {
path.slots[0] -= 1;
}
let Some(leaf) = path.nodes[0].as_mut() else {
path.release();
continue;
};
let slot = path.slots[0];
if slot >= leaf.nritems() as usize {
path.release();
continue;
}
let item_key = leaf.item_key(slot);
if item_key.key_type != KeyType::FreeSpaceExtent {
path.release();
continue;
}
let extent_start = item_key.objectid;
let extent_len = item_key.offset;
let extent_end = extent_start + extent_len;
if addr < extent_start || addr + nodesize > extent_end {
path.release();
continue;
}
items::del_items(leaf, slot, 1);
fs_info.mark_dirty(leaf);
path.release();
if addr > extent_start {
let left_key = DiskKey {
objectid: extent_start,
key_type: KeyType::FreeSpaceExtent,
offset: addr - extent_start,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&left_key,
&mut path,
SearchIntent::Insert(ITEM_SIZE as u32),
true,
)?;
let leaf = path.nodes[0].as_mut().unwrap();
items::insert_item(leaf, path.slots[0], &left_key, &[])?;
fs_info.mark_dirty(leaf);
path.release();
}
let after = addr + nodesize;
if after < extent_end {
let right_key = DiskKey {
objectid: after,
key_type: KeyType::FreeSpaceExtent,
offset: extent_end - after,
};
let mut path = BtrfsPath::new();
search::search_slot(
Some(&mut *self),
fs_info,
fst_id,
&right_key,
&mut path,
SearchIntent::Insert(ITEM_SIZE as u32),
true,
)?;
let leaf = path.nodes[0].as_mut().unwrap();
items::insert_item(leaf, path.slots[0], &right_key, &[])?;
fs_info.mark_dirty(leaf);
path.release();
}
}
Ok(())
}
pub fn abort(self, fs_info: &mut Filesystem<R>) {
fs_info.generation = fs_info.superblock.generation;
fs_info.clear_dirty();
fs_info.clear_cache();
fs_info.restore_roots_from_snapshot();
}
}
#[derive(Debug, Clone, Copy)]
struct InlineRefLocation {
inline_offset: usize,
inline_size: usize,
current_count: u32,
}
fn locate_inline_data_ref(
leaf: &crate::buffer::ExtentBuffer,
slot: usize,
target_root: u64,
target_ino: u64,
target_offset: u64,
) -> io::Result<Option<InlineRefLocation>> {
use btrfs_disk::items::{extent_data_ref_hash, inline_ref_size};
let item_key = leaf.item_key(slot);
let payload = leaf.item_data(slot);
if payload.len() < 24 {
return Err(io::Error::other(
"locate_inline_data_ref: EXTENT_ITEM payload too short",
));
}
let flags = u64::from_le_bytes(payload[16..24].try_into().unwrap());
let is_tree_block =
flags & u64::from(btrfs_disk::raw::BTRFS_EXTENT_FLAG_TREE_BLOCK) != 0;
let mut cursor = 24usize;
if is_tree_block && item_key.key_type == KeyType::ExtentItem {
cursor += 18;
}
if cursor > payload.len() {
return Err(io::Error::other(
"locate_inline_data_ref: header overruns payload",
));
}
let target_hash =
extent_data_ref_hash(target_root, target_ino, target_offset);
let edr_type = btrfs_disk::raw::BTRFS_EXTENT_DATA_REF_KEY as u8;
while cursor < payload.len() {
let type_byte = payload[cursor];
let size = inline_ref_size(type_byte).ok_or_else(|| {
io::Error::other(format!(
"locate_inline_data_ref: unknown inline ref type {type_byte}"
))
})?;
if cursor + size > payload.len() {
return Err(io::Error::other(
"locate_inline_data_ref: inline ref overruns payload",
));
}
if type_byte < edr_type {
cursor += size;
continue;
}
if type_byte > edr_type {
return Ok(None);
}
let body = &payload[cursor + 1..cursor + 1 + 28];
let r = u64::from_le_bytes(body[0..8].try_into().unwrap());
let o = u64::from_le_bytes(body[8..16].try_into().unwrap());
let off = u64::from_le_bytes(body[16..24].try_into().unwrap());
let count = u32::from_le_bytes(body[24..28].try_into().unwrap());
if r == target_root && o == target_ino && off == target_offset {
return Ok(Some(InlineRefLocation {
inline_offset: cursor,
inline_size: size,
current_count: count,
}));
}
let here_hash = extent_data_ref_hash(r, o, off);
if here_hash > target_hash {
return Ok(None);
}
cursor += size;
}
Ok(None)
}
fn decrement_inline_data_ref(
leaf: &mut crate::buffer::ExtentBuffer,
slot: usize,
location: &InlineRefLocation,
refs_to_drop: u32,
) -> io::Result<u64> {
if refs_to_drop > location.current_count {
return Err(io::Error::other(
"decrement_inline_data_ref: count underflow",
));
}
let item_data = leaf.item_data(slot);
let mut current_refs =
u64::from_le_bytes(item_data[0..8].try_into().unwrap());
if u64::from(refs_to_drop) > current_refs {
return Err(io::Error::other(
"decrement_inline_data_ref: EXTENT_ITEM.refs underflow",
));
}
current_refs -= u64::from(refs_to_drop);
leaf.item_data_mut(slot)[0..8].copy_from_slice(¤t_refs.to_le_bytes());
let new_count = location.current_count - refs_to_drop;
if new_count > 0 {
let count_off = location.inline_offset + 1 + 24;
leaf.item_data_mut(slot)[count_off..count_off + 4]
.copy_from_slice(&new_count.to_le_bytes());
return Ok(current_refs);
}
let item_size = leaf.item_size(slot) as usize;
let after_off = location.inline_offset + location.inline_size;
if after_off < item_size {
let payload = leaf.item_data_mut(slot);
payload.copy_within(after_off..item_size, location.inline_offset);
}
items::shrink_item(leaf, slot, location.inline_size as u32)?;
Ok(current_refs)
}
fn find_alloc_region_after<R: Read + Write + Seek>(
fs_info: &mut Filesystem<R>,
kind: BlockGroupKind,
min_addr: u64,
alignment: u64,
min_size: u64,
) -> io::Result<(u64, u64)> {
use crate::allocation;
let groups = allocation::load_block_groups(fs_info)?;
let kind_matches = |bg: &&allocation::BlockGroup| match kind {
BlockGroupKind::Metadata => bg.is_metadata(),
BlockGroupKind::System => bg.is_system(),
BlockGroupKind::Data => bg.is_data(),
};
let mut candidates: Vec<&allocation::BlockGroup> = groups
.iter()
.filter(kind_matches)
.filter(|bg| bg.free() >= min_size)
.collect();
candidates.sort_by_key(|bg| std::cmp::Reverse(bg.free()));
for bg in candidates {
let free_extents = allocation::find_free_extents(
fs_info, bg.start, bg.length, min_size,
)?;
for &(start, len) in &free_extents {
let cursor = align_up(start.max(min_addr), alignment);
let end = start + len;
if cursor + min_size <= end {
return Ok((cursor, end));
}
}
}
Err(io::Error::other(format!(
"no {kind:?} block group with free space",
)))
}
fn update_backup_root<R: Read + Write + Seek>(
fs_info: &mut Filesystem<R>,
slot: usize,
) {
use btrfs_disk::superblock::BackupRoot;
fn root_info<R: Read + Write + Seek>(
fs_info: &mut Filesystem<R>,
tree_id: u64,
) -> (u64, u64, u8) {
let bytenr = fs_info.root_bytenr(tree_id).unwrap_or(0);
if bytenr == 0 {
return (0, 0, 0);
}
match fs_info.read_block(bytenr) {
Ok(eb) => (bytenr, eb.generation(), eb.level()),
Err(_) => (bytenr, 0, 0),
}
}
let (tree_root, tree_root_gen, tree_root_level) = root_info(fs_info, 1);
let (chunk_root, chunk_root_gen, chunk_root_level) = root_info(fs_info, 3);
let (extent_root, extent_root_gen, extent_root_level) =
root_info(fs_info, 2);
let (fs_root, fs_root_gen, fs_root_level) = root_info(fs_info, 5);
let (dev_root, dev_root_gen, dev_root_level) = root_info(fs_info, 4);
let (csum_root, csum_root_gen, csum_root_level) = root_info(fs_info, 7);
fs_info.superblock.backup_roots[slot] = BackupRoot {
tree_root,
tree_root_gen,
chunk_root,
chunk_root_gen,
extent_root,
extent_root_gen,
fs_root,
fs_root_gen,
dev_root,
dev_root_gen,
csum_root,
csum_root_gen,
total_bytes: fs_info.superblock.total_bytes,
bytes_used: fs_info.superblock.bytes_used,
num_devices: fs_info.superblock.num_devices,
tree_root_level,
chunk_root_level,
extent_root_level,
fs_root_level,
dev_root_level,
csum_root_level,
};
}
fn find_containing_block_group(
groups: &[allocation::BlockGroup],
bytenr: u64,
) -> Option<u64> {
groups
.iter()
.find(|bg| bytenr >= bg.start && bytenr < bg.start + bg.length)
.map(|bg| bg.start)
}
const fn align_up(value: u64, align: u64) -> u64 {
(value + align - 1) & !(align - 1)
}
#[must_use]
pub fn try_compress(
data: &[u8],
algorithm: btrfs_disk::items::CompressionType,
) -> Option<Vec<u8>> {
use btrfs_disk::items::CompressionType;
if data.is_empty() {
return None;
}
let compressed = match algorithm {
CompressionType::None | CompressionType::Unknown(_) => return None,
CompressionType::Zlib => {
use flate2::{Compression, write::ZlibEncoder};
use std::io::Write;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::new(3));
enc.write_all(data).ok()?;
enc.finish().ok()?
}
CompressionType::Zstd => zstd::bulk::compress(data, 3).ok()?,
CompressionType::Lzo => {
let seg = lzokay::compress::compress(data).ok()?;
let total_len: u32 = (4 + 4 + seg.len()).try_into().ok()?;
let seg_len: u32 = seg.len().try_into().ok()?;
let mut buf = Vec::with_capacity(total_len as usize);
buf.extend_from_slice(&total_len.to_le_bytes());
buf.extend_from_slice(&seg_len.to_le_bytes());
buf.extend_from_slice(&seg);
buf
}
};
if compressed.len() < data.len() {
Some(compressed)
} else {
None
}
}
#[must_use]
pub fn try_compress_regular(
data: &[u8],
algorithm: btrfs_disk::items::CompressionType,
sectorsize: u32,
) -> Option<Vec<u8>> {
use btrfs_disk::items::CompressionType;
if data.is_empty() {
return None;
}
if !matches!(algorithm, CompressionType::Lzo) {
return try_compress(data, algorithm);
}
let ss = sectorsize as usize;
let sectors = data.len().div_ceil(ss);
let mut buf = Vec::with_capacity(data.len());
buf.extend_from_slice(&[0u8; 4]);
for i in 0..sectors {
let start = i * ss;
let end = (start + ss).min(data.len());
let seg = lzokay::compress::compress(&data[start..end]).ok()?;
let seg_len: u32 = seg.len().try_into().ok()?;
buf.extend_from_slice(&seg_len.to_le_bytes());
buf.extend_from_slice(&seg);
let pos = buf.len();
let sector_rem = ss - (pos % ss);
if sector_rem < 4 && sector_rem < ss {
buf.resize(pos + sector_rem, 0);
}
if i >= 3 && buf.len() > 3 * ss {
return None;
}
}
let total_len: u32 = buf.len().try_into().ok()?;
buf[0..4].copy_from_slice(&total_len.to_le_bytes());
if buf.len() < data.len() {
Some(buf)
} else {
None
}
}
#[must_use]
pub const fn max_inline_data_size(sectorsize: u32, nodesize: u32) -> usize {
let max_item_inline = (nodesize as usize) - HEADER_SIZE - ITEM_SIZE - 21;
let sector_cap = (sectorsize as usize) - 1;
if max_item_inline < sector_cap {
max_item_inline
} else {
sector_cap
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn align_up_already_aligned() {
assert_eq!(align_up(4096, 4096), 4096);
}
#[test]
fn align_up_not_aligned() {
assert_eq!(align_up(4097, 4096), 8192);
}
#[test]
fn align_up_zero() {
assert_eq!(align_up(0, 4096), 0);
}
#[test]
fn max_inline_default_nodesize_sectorsize() {
assert_eq!(max_inline_data_size(4096, 16384), 4095);
}
#[test]
fn max_inline_large_sectorsize_capped_by_nodesize() {
assert_eq!(max_inline_data_size(65536, 4096), 4096 - 147);
}
#[test]
fn try_compress_zlib_compressible() {
use btrfs_disk::items::CompressionType;
let data = vec![0x42u8; 4096];
let compressed = try_compress(&data, CompressionType::Zlib).unwrap();
assert!(compressed.len() < data.len());
}
#[test]
fn try_compress_zstd_compressible() {
use btrfs_disk::items::CompressionType;
let data = vec![0x42u8; 4096];
let compressed = try_compress(&data, CompressionType::Zstd).unwrap();
assert!(compressed.len() < data.len());
}
#[test]
fn try_compress_incompressible_returns_none() {
use btrfs_disk::items::CompressionType;
let mut state: u64 = 0xDEAD_BEEF_CAFE_F00D;
let data: Vec<u8> = (0..4096)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state as u8
})
.collect();
assert!(try_compress(&data, CompressionType::Zlib).is_none());
assert!(try_compress(&data, CompressionType::Zstd).is_none());
}
#[test]
fn try_compress_empty_returns_none() {
use btrfs_disk::items::CompressionType;
assert!(try_compress(&[], CompressionType::Zlib).is_none());
assert!(try_compress(&[], CompressionType::Zstd).is_none());
}
#[test]
fn try_compress_none_unknown_return_none() {
use btrfs_disk::items::CompressionType;
let data = vec![0x42u8; 4096];
assert!(try_compress(&data, CompressionType::None).is_none());
assert!(try_compress(&data, CompressionType::Unknown(99)).is_none());
}
#[test]
fn try_compress_lzo_inline_compressible() {
use btrfs_disk::items::CompressionType;
let data = vec![0x42u8; 4096];
let buf = try_compress(&data, CompressionType::Lzo).unwrap();
assert!(buf.len() < data.len());
let total_len =
u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
let seg_len =
u32::from_le_bytes(buf[4..8].try_into().unwrap()) as usize;
assert_eq!(total_len, buf.len(), "total_len matches buffer size");
assert_eq!(seg_len + 8, buf.len(), "seg_len + 8B header == total");
}
#[test]
fn try_compress_regular_lzo_compressible() {
use btrfs_disk::items::CompressionType;
let data = vec![0x42u8; 8192];
let buf =
try_compress_regular(&data, CompressionType::Lzo, 4096).unwrap();
assert!(buf.len() < data.len());
let total_len =
u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
assert_eq!(total_len, buf.len());
}
#[test]
fn try_compress_regular_lzo_incompressible_short_circuits() {
use btrfs_disk::items::CompressionType;
let mut state: u64 = 0xDEAD_BEEF_CAFE_F00D;
let data: Vec<u8> = (0..32 * 1024)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state as u8
})
.collect();
assert!(
try_compress_regular(&data, CompressionType::Lzo, 4096).is_none()
);
}
#[test]
fn try_compress_regular_zlib_zstd_match_inline() {
use btrfs_disk::items::CompressionType;
let data = vec![0x42u8; 4096];
for ct in [CompressionType::Zlib, CompressionType::Zstd] {
let inline_buf = try_compress(&data, ct).unwrap();
let regular_buf = try_compress_regular(&data, ct, 4096).unwrap();
assert_eq!(inline_buf, regular_buf, "{ct:?}");
}
}
}