use crate::common::single_mount;
use btrfs_uapi::{
filesystem::sync,
raw::{BTRFS_ROOT_ITEM_KEY, BTRFS_ROOT_TREE_OBJECTID},
subvolume::{subvolume_create, subvolume_info},
tree_search::{SearchFilter, tree_search, tree_search_v2},
};
use std::{
ffi::{CStr, CString},
fs::File,
os::unix::io::AsFd,
};
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_enumerate_root_items() {
let (_td, mnt) = single_mount();
let mut subvol_ids = Vec::new();
for name in [
CStr::from_bytes_with_nul(b"ts-a\0").unwrap(),
CStr::from_bytes_with_nul(b"ts-b\0").unwrap(),
CStr::from_bytes_with_nul(b"ts-c\0").unwrap(),
] {
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
let dir = File::open(mnt.path().join(name.to_str().unwrap()))
.expect("open failed");
let info = subvolume_info(dir.as_fd()).expect("subvolume_info failed");
subvol_ids.push(info.id);
}
sync(mnt.fd()).unwrap();
let mut found_ids = Vec::new();
tree_search(
mnt.fd(),
SearchFilter::for_type(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
),
|hdr, _data| {
found_ids.push(hdr.objectid);
Ok(())
},
)
.expect("tree_search failed");
for id in &subvol_ids {
assert!(
found_ids.contains(id),
"tree_search should find subvolume {id} in root items: found {found_ids:?}",
);
}
}
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_objectid_range() {
let (_td, mnt) = single_mount();
let mut subvol_ids = Vec::new();
for name in [
CStr::from_bytes_with_nul(b"range-a\0").unwrap(),
CStr::from_bytes_with_nul(b"range-b\0").unwrap(),
CStr::from_bytes_with_nul(b"range-c\0").unwrap(),
] {
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
let dir = File::open(mnt.path().join(name.to_str().unwrap()))
.expect("open failed");
let info = subvolume_info(dir.as_fd()).expect("subvolume_info failed");
subvol_ids.push(info.id);
}
sync(mnt.fd()).unwrap();
let target_id = subvol_ids[0];
let mut found = Vec::new();
tree_search(
mnt.fd(),
SearchFilter::for_objectid_range(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
target_id,
target_id,
),
|hdr, _data| {
found.push(hdr.objectid);
Ok(())
},
)
.expect("tree_search failed");
assert!(
found.contains(&target_id),
"should find the target objectid {target_id}: {found:?}",
);
for &other_id in &subvol_ids[1..] {
assert!(
!found.contains(&other_id),
"should not find objectid {other_id} outside the range: {found:?}",
);
}
}
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_empty_result() {
let (_td, mnt) = single_mount();
let mut invoked = false;
tree_search(
mnt.fd(),
SearchFilter::for_objectid_range(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
0,
0,
),
|_hdr, _data| {
invoked = true;
Ok(())
},
)
.expect("tree_search should succeed even with no results");
assert!(
!invoked,
"callback should not be invoked when no items match"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_large_result_no_duplicates() {
let (_td, mnt) = single_mount();
let mut expected_ids = Vec::new();
for i in 0..60 {
let name = CString::new(format!("sub-{i:03}")).unwrap();
subvolume_create(mnt.fd(), &name, &[])
.expect("subvolume_create failed");
let dir = File::open(mnt.path().join(name.to_str().unwrap()))
.expect("open failed");
let info = subvolume_info(dir.as_fd()).expect("subvolume_info failed");
expected_ids.push(info.id);
}
sync(mnt.fd()).unwrap();
let mut found_items: Vec<(u64, u64)> = Vec::new();
tree_search(
mnt.fd(),
SearchFilter::for_type(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
),
|hdr, _data| {
found_items.push((hdr.objectid, hdr.offset));
Ok(())
},
)
.expect("tree_search failed");
for id in &expected_ids {
assert!(
found_items.iter().any(|(oid, _)| oid == id),
"tree_search should find subvolume {id}",
);
}
let mut seen = std::collections::HashSet::new();
let mut dup_count = 0;
for &key in &found_items {
if !seen.insert(key) {
dup_count += 1;
}
}
assert_eq!(
dup_count, 0,
"should have no duplicate (objectid, offset) pairs, found {dup_count} duplicates"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_v2_matches_v1() {
let (_td, mnt) = single_mount();
for name in [
CStr::from_bytes_with_nul(b"v2-a\0").unwrap(),
CStr::from_bytes_with_nul(b"v2-b\0").unwrap(),
CStr::from_bytes_with_nul(b"v2-c\0").unwrap(),
] {
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
}
sync(mnt.fd()).unwrap();
let key = SearchFilter::for_type(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
);
let mut v1_items: Vec<(u64, u32, u64)> = Vec::new();
tree_search(mnt.fd(), key.clone(), |hdr, _data| {
v1_items.push((hdr.objectid, hdr.item_type, hdr.offset));
Ok(())
})
.expect("tree_search v1 failed");
let mut v2_items: Vec<(u64, u32, u64)> = Vec::new();
tree_search_v2(mnt.fd(), key, None, |hdr, _data| {
v2_items.push((hdr.objectid, hdr.item_type, hdr.offset));
Ok(())
})
.expect("tree_search_v2 failed");
assert_eq!(v1_items, v2_items, "v2 should return the same items as v1");
}
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_v2_small_buffer() {
let (_td, mnt) = single_mount();
for name in [
CStr::from_bytes_with_nul(b"sb-a\0").unwrap(),
CStr::from_bytes_with_nul(b"sb-b\0").unwrap(),
] {
subvolume_create(mnt.fd(), name, &[]).expect("subvolume_create failed");
}
sync(mnt.fd()).unwrap();
let key = SearchFilter::for_type(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
);
let mut items: Vec<u64> = Vec::new();
tree_search_v2(mnt.fd(), key, Some(4096), |hdr, _data| {
items.push(hdr.objectid);
Ok(())
})
.expect("tree_search_v2 with small buffer failed");
assert!(
!items.is_empty(),
"should find at least some root items even with small buffer"
);
}
#[test]
#[ignore = "requires elevated privileges"]
fn tree_search_v2_large_buffer() {
let (_td, mnt) = single_mount();
let key = SearchFilter::for_type(
BTRFS_ROOT_TREE_OBJECTID as u64,
BTRFS_ROOT_ITEM_KEY as u32,
);
let mut items: Vec<u64> = Vec::new();
tree_search_v2(mnt.fd(), key, Some(256 * 1024), |hdr, _data| {
items.push(hdr.objectid);
Ok(())
})
.expect("tree_search_v2 with large buffer failed");
assert!(
items.contains(&5),
"should find FS_TREE objectid 5: {items:?}"
);
}