use fs_btrfs::btree::{header_offsets as o, KeyPtr, HEADER_SIZE, ITEM_SIZE, KEY_PTR_SIZE};
use fs_btrfs::chunk::DiskKey;
use fs_btrfs::fs::Filesystem;
use fs_btrfs::superblock::Superblock;
use fs_btrfs::tree_write::{build_node, chunk_tree_uuid_of, key_ptr_capacity, BlockIdentity};
use fs_btrfs_test_support::{fixture, fixtures_matching, le32, le64, spans_several_devices, Image};
use fs_core::FileDevice;
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::Arc;
const ROOT_TREE: u64 = 1;
const ROOT_ITEM_KEY: u8 = 132;
const ROOT_ITEM_BYTENR: usize = 176;
fn images() -> Vec<PathBuf> {
let images: Vec<PathBuf> = fixtures_matching("btrfs-")
.into_iter()
.filter(|p| !spans_several_devices(p))
.collect();
assert!(
!images.is_empty(),
"every fixture belongs to a multi-device filesystem, so there is no image here \
to scan for nodes"
);
images
}
fn ptrs_of(block: &[u8]) -> Vec<KeyPtr> {
let nritems = le32(block, o::NRITEMS) as usize;
(0..nritems)
.map(|i| {
let at = HEADER_SIZE + i * KEY_PTR_SIZE;
KeyPtr {
key: DiskKey {
objectid: le64(block, at),
key_type: block[at + 8],
offset: le64(block, at + 9),
},
blockptr: le64(block, at + 17),
generation: le64(block, at + 25),
}
})
.collect()
}
fn nodes(img: &Path) -> (Superblock, Vec<Vec<u8>>) {
let sb = superblock(img);
let image = Image::open(img);
let nodesize = sb.nodesize as usize;
let mut blocks = Vec::new();
let mut buf = vec![0u8; nodesize];
let mut at = 0u64;
while image.try_read_at(at, &mut buf) {
let block = &buf[..];
at += nodesize as u64;
if block[o::FSID..o::FSID + 16] != sb.fsid[..] || block[o::LEVEL] == 0 {
continue;
}
let nritems = le32(block, o::NRITEMS) as usize;
if nritems == 0 || HEADER_SIZE + nritems * KEY_PTR_SIZE > nodesize {
continue;
}
if !sb.csum_type.verify(&block[32..], &block[..32]) {
continue;
}
blocks.push(block.to_vec());
}
(sb, blocks)
}
fn superblock(img: &Path) -> Superblock {
let dev = Arc::new(
FileDevice::open(img).unwrap_or_else(|e| panic!("opening {}: {e}", img.display())),
);
Filesystem::mount(dev)
.unwrap_or_else(|e| panic!("mounting {}: {e}", img.display()))
.superblock()
.clone()
}
#[test]
fn every_node_re_encodes_identically() {
let mut total = 0usize;
let mut exact = 0usize;
let mut images_with_nodes = 0usize;
let mut deepest = 0u8;
let images = images();
for img in &images {
let (sb, blocks) = nodes(img);
if blocks.is_empty() {
continue;
}
images_with_nodes += 1;
let name = img.file_name().unwrap().to_string_lossy().into_owned();
for theirs in &blocks {
let ptrs = ptrs_of(theirs);
let level = theirs[o::LEVEL];
deepest = deepest.max(level);
let id = BlockIdentity {
bytenr: le64(theirs, o::BYTENR),
owner: le64(theirs, o::OWNER),
generation: le64(theirs, o::GENERATION),
level,
flags: le64(theirs, o::FLAGS),
chunk_tree_uuid: chunk_tree_uuid_of(theirs),
};
let ours = match build_node(&sb, id, &ptrs) {
Ok(b) => b,
Err(e) => panic!(
"{name}: node at {} was refused: {e}. The kernel wrote it, so \
refusing it names a rule this encoder has that the format does not.",
id.bytenr
),
};
let ptrs_end = HEADER_SIZE + ptrs.len() * KEY_PTR_SIZE;
if let Some(i) = (32..ptrs_end).find(|&i| ours[i] != theirs[i]) {
let (what, within) = if i < HEADER_SIZE {
("the header", i)
} else {
("key pointer", (i - HEADER_SIZE) % KEY_PTR_SIZE)
};
panic!(
"{name}: node at {} differs at byte {i:#x} — {what}, offset {within} \
into it (ours {:#04x}, kernel {:#04x}). A key pointer is a 17-byte \
key, then blockptr at 17 and generation at 25.",
id.bytenr, ours[i], theirs[i]
);
}
if ours[ptrs_end..] == theirs[ptrs_end..] {
assert_eq!(
ours, *theirs,
"{name}: node at {} has identical content and slack, so every byte \
including the checksum should match",
id.bytenr
);
exact += 1;
} else {
assert!(
sb.csum_type.verify(&ours[32..], &ours[..32]),
"{name}: node at {} carries a checksum that does not cover it",
id.bytenr
);
}
total += 1;
}
}
assert!(
images_with_nodes > 0,
"{} fixtures were read and not one had a tree above level 0, so this test \
exercised nothing. The deep fixtures — the `populated` target of \
`chore fixtures` — are what produce nodes.",
images.len()
);
eprintln!(
"{total} kernel nodes rebuilt across {images_with_nodes} images, deepest level \
{deepest}; {exact} matched every byte including the checksum"
);
assert!(
exact > 0,
"not one node matched byte for byte — if the slack always differs, the comparison \
never reaches the checksum"
);
}
fn blocks_by_bytenr(image: &Image, sb: &Superblock) -> HashMap<u64, Vec<u8>> {
let nodesize = sb.nodesize as usize;
let mut index: HashMap<u64, Vec<u8>> = HashMap::new();
let mut buf = vec![0u8; nodesize];
let mut at = 0u64;
while image.try_read_at(at, &mut buf) {
let block = &buf[..];
at += nodesize as u64;
if block[o::FSID..o::FSID + 16] != sb.fsid[..] {
continue;
}
let nritems = le32(block, o::NRITEMS) as usize;
if nritems == 0 || HEADER_SIZE + nritems * KEY_PTR_SIZE > nodesize {
continue;
}
if !sb.csum_type.verify(&block[32..], &block[..32]) {
continue;
}
let bytenr = le64(block, o::BYTENR);
let generation = le64(block, o::GENERATION);
match index.get(&bytenr) {
Some(seen) if le64(seen, o::GENERATION) >= generation => {}
_ => {
index.insert(bytenr, block.to_vec());
}
}
}
index
}
fn live_nodes(index: &HashMap<u64, Vec<u8>>, sb: &Superblock) -> Vec<Vec<u8>> {
let mut queue: VecDeque<u64> = [sb.root, sb.chunk_root, sb.log_root]
.into_iter()
.filter(|&b| b != 0)
.collect();
let mut seen: HashSet<u64> = queue.iter().copied().collect();
let mut nodes = Vec::new();
while let Some(bytenr) = queue.pop_front() {
let Some(block) = index.get(&bytenr) else {
continue;
};
let nritems = le32(block, o::NRITEMS) as usize;
if block[o::LEVEL] > 0 {
for ptr in ptrs_of(block) {
if seen.insert(ptr.blockptr) {
queue.push_back(ptr.blockptr);
}
}
nodes.push(block.clone());
continue;
}
if le64(block, o::OWNER) != ROOT_TREE {
continue;
}
for i in 0..nritems {
let item = HEADER_SIZE + i * ITEM_SIZE;
if item + ITEM_SIZE > block.len() || block[item + 8] != ROOT_ITEM_KEY {
continue;
}
let at = HEADER_SIZE + le32(block, item + 17) as usize;
let size = le32(block, item + 21) as usize;
if at + size > block.len() || size < ROOT_ITEM_BYTENR + 8 {
continue;
}
let root = le64(block, at + ROOT_ITEM_BYTENR);
if root != 0 && seen.insert(root) {
queue.push_back(root);
}
}
}
nodes
}
#[test]
fn each_pointer_key_is_the_first_key_of_the_child_it_names() {
let mut checked = 0usize;
let mut walked = 0usize;
let images = images();
for img in &images {
let sb = superblock(img);
let index = blocks_by_bytenr(&Image::open(img), &sb);
let nodes = live_nodes(&index, &sb);
walked += nodes.len();
for node in &nodes {
for ptr in ptrs_of(node) {
let child = index.get(&ptr.blockptr).unwrap_or_else(|| {
panic!(
"{}: a live node names a child at {} that is not on this image, \
so the tree the superblock reaches is incomplete",
img.display(),
ptr.blockptr
)
});
let first = DiskKey {
objectid: le64(child, HEADER_SIZE),
key_type: child[HEADER_SIZE + 8],
offset: le64(child, HEADER_SIZE + 9),
};
assert_eq!(
(ptr.key.objectid, ptr.key.key_type, ptr.key.offset),
(first.objectid, first.key_type, first.offset),
"{}: a pointer to {} carries {:?} but the child's first key is {:?}",
img.display(),
ptr.blockptr,
ptr.key,
first
);
checked += 1;
}
}
}
assert!(
checked > 0,
"not one parent/child pair was reachable across {} fixtures, so the descent \
invariant was never checked",
images.len()
);
eprintln!(
"{checked} pointer keys are the first key of the child they name, across {walked} \
live nodes"
);
}
#[test]
fn a_node_cannot_claim_to_be_a_leaf() {
let sb = superblock(&fixture("btrfs-default.img"));
let ptr = KeyPtr {
key: DiskKey {
objectid: 1,
key_type: 1,
offset: 0,
},
blockptr: 4096,
generation: 1,
};
let err = build_node(
&sb,
BlockIdentity {
bytenr: 0,
owner: 1,
generation: 1,
level: 0,
flags: 0,
chunk_tree_uuid: [0; 16],
},
&[ptr],
)
.expect_err("level 0 with key pointers is a contradiction");
assert!(
err.to_string().contains("level 0"),
"the refusal should name the contradiction: {err}"
);
}
#[test]
fn an_overfull_node_is_refused() {
let sb = superblock(&fixture("btrfs-default.img"));
let capacity = key_ptr_capacity(&sb);
let ptrs: Vec<KeyPtr> = (0..capacity as u64 + 1)
.map(|i| KeyPtr {
key: DiskKey {
objectid: i,
key_type: 1,
offset: 0,
},
blockptr: 4096 * (i + 1),
generation: 1,
})
.collect();
assert!(
build_node(
&sb,
BlockIdentity {
bytenr: 0,
owner: 1,
generation: 1,
level: 1,
flags: 0,
chunk_tree_uuid: [0; 16],
},
&ptrs,
)
.is_err(),
"one more than capacity ({capacity}) must be refused"
);
assert!(
build_node(
&sb,
BlockIdentity {
bytenr: 0,
owner: 1,
generation: 1,
level: 1,
flags: 0,
chunk_tree_uuid: [0; 16],
},
&ptrs[..capacity],
)
.is_ok(),
"exactly capacity ({capacity}) must fit"
);
}