use std::alloc::{alloc_zeroed, Layout};
use std::collections::BTreeSet;
use std::convert::TryInto;
use std::fs::{File, OpenOptions};
use std::mem::size_of;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::slice::SliceIndex;
use anyhow::{anyhow, bail, ensure, Result};
use log::info;
use memmap2::MmapMut;
use nix::dir::Dir;
use nix::errno::Errno;
use nix::fcntl::OFlag;
use nix::ioctl_readwrite;
use nix::sys::{stat::Mode, statfs::fstatfs};
const BTRFS_SUPERBLOCK_MAGIC: [u8; 8] = *b"_BHRfS_M";
const BTRFS_SUPERBLOCK_MAGIC_LOCS: [usize; 3] =
[0x1_0000 + 0x40, 0x400_0000 + 0x40, 0x40_0000_0000 + 0x40];
const BTRFS_FSTYPE: i64 = 0x9123683e;
const BTRFS_IOC_MAGIC: usize = 0x94;
const MAX_BUF_SIZE: usize = 16 << 20;
ioctl_readwrite!(
btrfs_tree_search_v2,
BTRFS_IOC_MAGIC,
17,
BtrfsIoctlSearchArgsV2
);
#[repr(C)]
#[derive(Default, Debug)]
pub struct BtrfsIoctlSearchKey {
tree_id: u64,
min_objectid: u64,
max_objectid: u64,
min_offset: u64,
max_offset: u64,
min_transid: u64,
max_transid: u64,
min_type: u32,
max_type: u32,
nr_items: u32,
_unused: u32,
_unused1: u64,
_unused2: u64,
_unused3: u64,
_unused4: u64,
}
#[repr(C)]
pub struct BtrfsIoctlSearchArgsV2 {
key: BtrfsIoctlSearchKey,
buf_size: u64,
buf: [u8; MAX_BUF_SIZE],
}
impl BtrfsIoctlSearchArgsV2 {
fn new() -> Box<Self> {
let mut args: Box<Self> = unsafe {
let ptr = alloc_zeroed(Layout::new::<Self>());
Box::from_raw(ptr as _)
};
args.key = BtrfsIoctlSearchKey::default();
args.key.nr_items = u32::MAX;
args.buf_size = MAX_BUF_SIZE.try_into().unwrap();
args
}
}
#[repr(C)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct BtrfsIoctlSearchHeader {
pub transid: u64,
pub objectid: u64,
pub offset: u64,
pub ty: u32,
pub len: u32,
}
enum FsInner {
Mounted(Dir),
Unmounted(MmapMut),
}
pub struct Fs {
inner: FsInner,
}
impl Fs {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
if let Ok(d) = Dir::open(
path.as_ref(),
OFlag::O_DIRECTORY | OFlag::O_RDONLY,
Mode::empty(),
) {
Self::new_mounted(d, path)
} else if let Ok(f) = OpenOptions::new()
.read(true)
.write(true)
.open(path.as_ref())
{
Self::new_unmounted(f, path)
} else {
bail!(
"{} is neither a mounted nor unmounted btrfs filesystem",
path.as_ref().display()
);
}
}
fn new_mounted<P: AsRef<Path>>(fs: Dir, path: P) -> Result<Self> {
let statfs = fstatfs(&fs)?;
ensure!(
i64::from(statfs.filesystem_type().0) == BTRFS_FSTYPE,
"{} is not a btrfs filesystem",
path.as_ref().display()
);
Ok(Self {
inner: FsInner::Mounted(fs),
})
}
fn has_superblock_magic(fs: &[u8]) -> bool {
for loc in &BTRFS_SUPERBLOCK_MAGIC_LOCS {
if let Some(magic) = fs.get(*loc..(loc + 8)) {
if magic == BTRFS_SUPERBLOCK_MAGIC {
return true;
}
}
}
false
}
fn new_unmounted<P: AsRef<Path>>(fs: File, path: P) -> Result<Self> {
let mmap = unsafe { MmapMut::map_mut(&fs) }?;
ensure!(
Self::has_superblock_magic(&*mmap),
"Failed to find valid superblock magic for: {}",
path.as_ref().display(),
);
Ok(Self {
inner: FsInner::Unmounted(mmap),
})
}
#[allow(clippy::too_many_arguments)]
pub fn search(
&self,
tree_id: u64,
mut min_objectid: u64,
max_objectid: u64,
mut min_type: u8,
max_type: u8,
mut min_offset: u64,
max_offset: u64,
min_transid: u64,
max_transid: u64,
) -> Result<Vec<(BtrfsIoctlSearchHeader, Vec<u8>)>> {
let fs = match &self.inner {
FsInner::Mounted(fs) => fs,
FsInner::Unmounted(_) => bail!("Cannot search() an unmounted filesystem"),
};
let retry = max_objectid != u64::MAX
|| max_type != u8::MAX
|| max_offset != u64::MAX
|| max_transid != u64::MAX;
let mut ret = Vec::new();
let mut seen = BTreeSet::new();
let mut collisions = 0;
loop {
let mut args = BtrfsIoctlSearchArgsV2::new();
args.key.tree_id = tree_id;
args.key.min_objectid = min_objectid;
args.key.max_objectid = max_objectid;
args.key.min_type = min_type.into();
args.key.max_type = max_type.into();
args.key.min_offset = min_offset;
args.key.max_offset = max_offset;
args.key.min_transid = min_transid;
args.key.max_transid = max_transid;
match unsafe { btrfs_tree_search_v2(fs.as_raw_fd(), &mut *args) } {
Ok(_) => (),
Err(Errno::EOVERFLOW) => (),
Err(e) => bail!(e),
};
info!("search() key: {:#?}", args.key);
info!("search() found {} items", args.key.nr_items);
let mut offset: usize = 0;
for _ in 0..args.key.nr_items {
let header_sz = size_of::<BtrfsIoctlSearchHeader>();
ensure!(
header_sz + offset <= MAX_BUF_SIZE,
"BtrfsIoctlSearchHeader short read"
);
let header = unsafe {
(&*(args.buf[offset..].as_ptr() as *const BtrfsIoctlSearchHeader)).clone()
};
offset += header_sz;
ensure!(
header.len as usize + offset <= MAX_BUF_SIZE,
"BtrfsIoctlSearchHeader payload short read"
);
let bytes = args.buf[offset..(offset + header.len as usize)].to_vec();
offset += header.len as usize;
if seen.contains(&header) {
collisions += 1;
} else {
seen.insert(header.clone());
ret.push((header, bytes));
}
}
if !retry || args.key.nr_items == 0 {
break;
}
let largest = ret
.iter()
.map(|(header, _)| header)
.max_by(|lhs, rhs| {
use std::cmp::Ordering;
if lhs.objectid < rhs.objectid {
Ordering::Less
} else if lhs.objectid > rhs.objectid {
Ordering::Greater
} else if lhs.ty < rhs.ty {
Ordering::Less
} else if lhs.ty > rhs.ty {
Ordering::Greater
} else if lhs.offset < rhs.offset {
Ordering::Less
} else if lhs.offset > rhs.offset {
Ordering::Greater
} else {
Ordering::Equal
}
})
.unwrap();
min_objectid = largest.objectid;
min_type = largest.ty.try_into()?;
min_offset = largest
.offset
.checked_add(1)
.ok_or_else(|| anyhow!("Could not bump key.offset -- overflow detected"))?;
}
if collisions != 0 {
bail!(
"Saw {} identical payloads -- this should not be possible",
collisions
);
}
Ok(ret)
}
pub fn get_bytes<I>(&self, index: I) -> Result<&I::Output>
where
I: SliceIndex<[u8]> + std::fmt::Debug,
{
match &self.inner {
FsInner::Unmounted(fs) => fs
.get(index)
.ok_or_else(|| anyhow!("Index out of range, max length is {}", fs.len())),
FsInner::Mounted(_) => bail!("Cannot reinterpret bytes from mounted image"),
}
}
}