use super::errors::{CheckError, CheckResults};
use btrfs_disk::{
items::{
DirItem, FileExtentBody, FileExtentItem, FileExtentType, InodeExtref,
InodeItem, InodeRef,
},
raw,
reader::{self, BlockReader},
tree::{KeyType, TreeBlock},
};
use std::{
collections::{BTreeMap, HashSet},
io::{Read, Seek},
};
const HEADER_SIZE: usize = std::mem::size_of::<btrfs_disk::raw::btrfs_header>();
pub fn check_fs_roots<R: Read + Seek>(
reader: &mut BlockReader<R>,
tree_roots: &BTreeMap<u64, (u64, u64)>,
results: &mut CheckResults,
) {
for (&tree_id, &(bytenr, _gen)) in tree_roots {
let is_fs_tree = tree_id == u64::from(raw::BTRFS_FS_TREE_OBJECTID)
|| tree_id >= u64::from(raw::BTRFS_FIRST_FREE_OBJECTID);
if !is_fs_tree {
continue;
}
check_one_fs_tree(reader, tree_id, bytenr, results);
}
}
const S_IFMT: u32 = 0o17_0000;
const S_IFDIR: u32 = 0o04_0000;
const S_IFREG: u32 = 0o10_0000;
const S_IFLNK: u32 = 0o12_0000;
#[allow(clippy::too_many_lines)]
fn check_one_fs_tree<R: Read + Seek>(
reader: &mut BlockReader<R>,
tree_id: u64,
root_bytenr: u64,
results: &mut CheckResults,
) {
let Some(items) = collect_fs_items(reader, root_bytenr, results) else {
return;
};
let inodes_with_item: HashSet<u64> = items
.iter()
.filter(|(_, entries)| {
entries.iter().any(|(kt, _, _)| *kt == KeyType::InodeItem)
})
.map(|(&ino, _)| ino)
.collect();
for (&ino, entries) in &items {
let mut has_inode_item = false;
let mut inode_nlink: u32 = 0;
let mut inode_size: u64 = 0;
let mut inode_nbytes: u64 = 0;
let mut inode_mode: u32 = 0;
let mut ref_count: u32 = 0;
let mut extent_ranges: Vec<(u64, u64)> = Vec::new();
let mut dir_index_name_sum: u64 = 0;
let mut computed_nbytes: u64 = 0;
for (key_type, key_offset, data) in entries {
match key_type {
KeyType::InodeItem => {
has_inode_item = true;
if let Some(ii) = InodeItem::parse(data) {
inode_nlink = ii.nlink;
inode_size = ii.size;
inode_nbytes = ii.nbytes;
inode_mode = ii.mode;
}
}
KeyType::InodeRef => {
for _r in InodeRef::parse_all(data) {
ref_count += 1;
}
}
KeyType::InodeExtref => {
for _r in InodeExtref::parse_all(data) {
ref_count += 1;
}
}
KeyType::ExtentData => {
if let Some(fe) = FileExtentItem::parse(data) {
let len = match &fe.body {
FileExtentBody::Regular { num_bytes, .. } => {
*num_bytes
}
FileExtentBody::Inline { inline_size } => {
*inline_size as u64
}
};
if len > 0 {
extent_ranges
.push((*key_offset, *key_offset + len));
}
match &fe.body {
FileExtentBody::Inline { inline_size } => {
computed_nbytes += *inline_size as u64;
}
FileExtentBody::Regular {
disk_num_bytes, ..
} => {
if fe.extent_type != FileExtentType::Prealloc {
computed_nbytes += disk_num_bytes;
}
}
}
}
}
KeyType::DirItem => {
let dir_items = DirItem::parse_all(data);
for di in &dir_items {
let child_ino = di.location.objectid;
if di.location.key_type == KeyType::InodeItem
&& child_ino
>= u64::from(raw::BTRFS_FIRST_FREE_OBJECTID)
&& !inodes_with_item.contains(&child_ino)
{
let name =
String::from_utf8_lossy(&di.name).into_owned();
results.report(CheckError::DirItemOrphan {
tree: tree_id,
parent_ino: ino,
name,
});
}
}
}
KeyType::DirIndex => {
let dir_items = DirItem::parse_all(data);
for di in &dir_items {
dir_index_name_sum += di.name.len() as u64 * 2;
let child_ino = di.location.objectid;
if di.location.key_type == KeyType::InodeItem
&& child_ino
>= u64::from(raw::BTRFS_FIRST_FREE_OBJECTID)
&& !inodes_with_item.contains(&child_ino)
{
let name =
String::from_utf8_lossy(&di.name).into_owned();
results.report(CheckError::DirItemOrphan {
tree: tree_id,
parent_ino: ino,
name,
});
}
}
}
_ => {}
}
}
for i in 1..extent_ranges.len() {
let prev_end = extent_ranges[i - 1].1;
let cur_start = extent_ranges[i].0;
if cur_start < prev_end {
results.report(CheckError::FileExtentOverlap {
tree: tree_id,
ino,
offset: cur_start,
});
}
}
if has_inode_item
&& ino >= u64::from(raw::BTRFS_FIRST_FREE_OBJECTID)
&& inode_nlink != ref_count
&& ref_count > 0
{
results.report(CheckError::NlinkMismatch {
tree: tree_id,
ino,
expected: inode_nlink,
found: ref_count,
});
}
if !has_inode_item {
continue;
}
let file_type = inode_mode & S_IFMT;
if file_type == S_IFDIR && inode_size != dir_index_name_sum {
results.report(CheckError::DirSizeWrong {
tree: tree_id,
ino,
expected: dir_index_name_sum,
found: inode_size,
});
}
if (file_type == S_IFREG || file_type == S_IFLNK)
&& inode_nbytes != computed_nbytes
{
results.report(CheckError::NbytesWrong {
tree: tree_id,
ino,
expected: computed_nbytes,
found: inode_nbytes,
});
}
}
}
type FsItemMap = BTreeMap<u64, Vec<(KeyType, u64, Vec<u8>)>>;
fn collect_fs_items<R: Read + Seek>(
reader: &mut BlockReader<R>,
root_bytenr: u64,
results: &mut CheckResults,
) -> Option<FsItemMap> {
let mut items: FsItemMap = BTreeMap::new();
let mut read_errors: Vec<(u64, String)> = Vec::new();
let mut visitor = |_raw: &[u8], block: &TreeBlock| {
if let TreeBlock::Leaf {
items: leaf_items,
data,
..
} = block
{
for item in leaf_items {
let start = HEADER_SIZE + item.offset as usize;
let item_data = data[start..][..item.size as usize].to_vec();
items.entry(item.key.objectid).or_default().push((
item.key.key_type,
item.key.offset,
item_data,
));
}
}
};
let mut on_error = |logical: u64, err: &std::io::Error| {
read_errors.push((logical, err.to_string()));
};
if let Err(e) = reader::tree_walk_tolerant(
reader,
root_bytenr,
&mut visitor,
&mut on_error,
) {
results.report(CheckError::ReadError {
logical: root_bytenr,
detail: format!("fs tree root: {e}"),
});
return None;
}
for (logical, detail) in read_errors {
results.report(CheckError::ReadError { logical, detail });
}
Some(items)
}