use btrfs_fs::{Filesystem, Inode, RootRef, SearchFilter, SubvolId};
use bytes::{Buf, BufMut};
use fuser::Errno;
use std::fs::File;
const IOC_NRSHIFT: u32 = 0;
const IOC_TYPESHIFT: u32 = 8;
const IOC_SIZESHIFT: u32 = 16;
const IOC_DIRSHIFT: u32 = 30;
const IOC_READ: u32 = 2;
const fn ioc(dir: u32, ty: u8, nr: u8, size: u32) -> u32 {
(dir << IOC_DIRSHIFT)
| ((ty as u32) << IOC_TYPESHIFT)
| ((nr as u32) << IOC_NRSHIFT)
| (size << IOC_SIZESHIFT)
}
const fn ior(ty: u8, nr: u8, size: u32) -> u32 {
ioc(IOC_READ, ty, nr, size)
}
const fn iowr(ty: u8, nr: u8, size: u32) -> u32 {
ioc(IOC_READ | 1, ty, nr, size)
}
const BTRFS_MAGIC: u8 = 0x94;
const FS_INFO_SIZE: u32 = 1024;
const FEATURE_FLAGS_SIZE: u32 = 24;
const SUBVOL_INFO_SIZE: u32 = 504;
const DEV_INFO_SIZE: u32 = 4096;
const INO_LOOKUP_SIZE: u32 = 4096;
const SEARCH_ARGS_V1_SIZE: u32 = 4096;
const SEARCH_ARGS_V1_BUF: usize = 3992;
const SEARCH_ARGS_V2_SIZE: u32 = 112;
const SEARCH_KEY_SIZE: usize = 104;
#[allow(dead_code)]
const SEARCH_HEADER_SIZE: usize = 32;
const SUBVOL_ROOTREF_SIZE: u32 = 4096;
const MAX_ROOTREF_BUFFER_NUM: usize = 255;
pub const BTRFS_IOC_FS_INFO: u32 = ior(BTRFS_MAGIC, 31, FS_INFO_SIZE);
pub const BTRFS_IOC_GET_FEATURES: u32 =
ior(BTRFS_MAGIC, 57, FEATURE_FLAGS_SIZE);
pub const BTRFS_IOC_GET_SUBVOL_INFO: u32 =
ior(BTRFS_MAGIC, 60, SUBVOL_INFO_SIZE);
pub const BTRFS_IOC_DEV_INFO: u32 = iowr(BTRFS_MAGIC, 30, DEV_INFO_SIZE);
pub const BTRFS_IOC_INO_LOOKUP: u32 = iowr(BTRFS_MAGIC, 18, INO_LOOKUP_SIZE);
pub const BTRFS_IOC_TREE_SEARCH: u32 =
iowr(BTRFS_MAGIC, 17, SEARCH_ARGS_V1_SIZE);
pub const BTRFS_IOC_TREE_SEARCH_V2: u32 =
iowr(BTRFS_MAGIC, 17, SEARCH_ARGS_V2_SIZE);
pub const BTRFS_IOC_GET_SUBVOL_ROOTREF: u32 =
iowr(BTRFS_MAGIC, 61, SUBVOL_ROOTREF_SIZE);
pub enum IoctlOutcome {
Ok(Vec<u8>),
Err(Errno),
}
pub async fn dispatch(
fs: &Filesystem<File>,
target: Inode,
cmd: u32,
in_data: &[u8],
) -> IoctlOutcome {
match cmd {
BTRFS_IOC_FS_INFO => fs_info(fs),
BTRFS_IOC_GET_FEATURES => get_features(fs),
BTRFS_IOC_GET_SUBVOL_INFO => get_subvol_info(fs, target.subvol).await,
BTRFS_IOC_DEV_INFO => dev_info(fs, in_data),
BTRFS_IOC_INO_LOOKUP => ino_lookup(fs, target.subvol, in_data).await,
BTRFS_IOC_TREE_SEARCH => {
tree_search_v1(fs, target.subvol, in_data).await
}
BTRFS_IOC_TREE_SEARCH_V2 => tree_search_v2(),
BTRFS_IOC_GET_SUBVOL_ROOTREF => {
get_subvol_rootref(fs, target.subvol, in_data).await
}
_ => IoctlOutcome::Err(Errno::ENOTTY),
}
}
fn fs_info(fs: &Filesystem<File>) -> IoctlOutcome {
let sb = fs.superblock();
let mut buf: Vec<u8> = Vec::with_capacity(FS_INFO_SIZE as usize);
let max_id = sb.num_devices.max(1);
buf.put_u64_le(max_id);
buf.put_u64_le(sb.num_devices);
buf.put_slice(sb.fsid.as_bytes());
buf.put_u32_le(sb.nodesize);
buf.put_u32_le(sb.sectorsize);
buf.put_u32_le(sb.sectorsize); buf.put_u16_le(sb.csum_type.to_raw());
#[allow(clippy::cast_possible_truncation)]
buf.put_u16_le(sb.csum_type.size() as u16);
buf.put_u64_le(0); buf.put_u64_le(sb.generation);
buf.put_slice(sb.metadata_uuid.as_bytes());
buf.resize(FS_INFO_SIZE as usize, 0);
debug_assert_eq!(buf.len(), FS_INFO_SIZE as usize);
IoctlOutcome::Ok(buf)
}
fn get_features(fs: &Filesystem<File>) -> IoctlOutcome {
let sb = fs.superblock();
let mut buf: Vec<u8> = Vec::with_capacity(FEATURE_FLAGS_SIZE as usize);
buf.put_u64_le(sb.compat_flags);
buf.put_u64_le(sb.compat_ro_flags);
buf.put_u64_le(sb.incompat_flags);
debug_assert_eq!(buf.len(), FEATURE_FLAGS_SIZE as usize);
IoctlOutcome::Ok(buf)
}
async fn get_subvol_info(
fs: &Filesystem<File>,
subvol: SubvolId,
) -> IoctlOutcome {
let info = match fs.get_subvol_info(subvol).await {
Ok(Some(info)) => info,
Ok(None) => return IoctlOutcome::Err(Errno::ENOENT),
Err(e) => {
log::warn!("ioctl GET_SUBVOL_INFO subvol={}: {e}", subvol.0);
return IoctlOutcome::Err(Errno::EIO);
}
};
let mut buf: Vec<u8> = Vec::with_capacity(SUBVOL_INFO_SIZE as usize);
buf.put_u64_le(info.id.0);
let mut name_buf = [0u8; 256];
let n = info.name.len().min(255);
name_buf[..n].copy_from_slice(&info.name[..n]);
buf.put_slice(&name_buf);
buf.put_u64_le(info.parent.map_or(0, |p| p.0));
buf.put_u64_le(info.dirid);
buf.put_u64_le(info.generation);
let flags: u64 = if info.readonly { 1 << 0 } else { 0 }; buf.put_u64_le(flags);
buf.put_slice(info.uuid.as_bytes());
buf.put_slice(info.parent_uuid.as_bytes());
buf.put_slice(info.received_uuid.as_bytes());
buf.put_u64_le(info.ctransid);
buf.put_u64_le(info.otransid);
buf.put_u64_le(0); buf.put_u64_le(0);
write_timespec(&mut buf, info.ctime);
write_timespec(&mut buf, info.otime);
write_timespec(&mut buf, std::time::SystemTime::UNIX_EPOCH); write_timespec(&mut buf, std::time::SystemTime::UNIX_EPOCH);
for _ in 0..8 {
buf.put_u64_le(0);
}
debug_assert_eq!(buf.len(), SUBVOL_INFO_SIZE as usize);
IoctlOutcome::Ok(buf)
}
fn write_timespec(buf: &mut Vec<u8>, t: std::time::SystemTime) {
let dur = t.duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
buf.put_u64_le(dur.as_secs());
buf.put_u32_le(dur.subsec_nanos());
buf.put_u32_le(0); }
fn dev_info(fs: &Filesystem<File>, in_data: &[u8]) -> IoctlOutcome {
if in_data.len() < DEV_INFO_SIZE as usize {
return IoctlOutcome::Err(Errno::EINVAL);
}
let mut cursor = in_data;
let req_devid = cursor.get_u64_le();
let mut req_uuid = [0u8; 16];
cursor.copy_to_slice(&mut req_uuid);
let dev = if req_devid != 0 {
fs.dev_info(req_devid)
} else {
let primary = fs.dev_info(1);
primary.filter(|d| d.uuid.as_bytes() == &req_uuid)
};
let Some(dev) = dev else {
return IoctlOutcome::Err(Errno::ENODEV);
};
let mut buf: Vec<u8> = Vec::with_capacity(DEV_INFO_SIZE as usize);
buf.put_u64_le(dev.devid);
buf.put_slice(dev.uuid.as_bytes());
buf.put_u64_le(dev.bytes_used);
buf.put_u64_le(dev.total_bytes);
buf.resize(buf.len() + 379 * 8, 0);
buf.resize(DEV_INFO_SIZE as usize, 0);
debug_assert_eq!(buf.len(), DEV_INFO_SIZE as usize);
IoctlOutcome::Ok(buf)
}
async fn ino_lookup(
fs: &Filesystem<File>,
current_subvol: SubvolId,
in_data: &[u8],
) -> IoctlOutcome {
if in_data.len() < INO_LOOKUP_SIZE as usize {
return IoctlOutcome::Err(Errno::EINVAL);
}
let mut cursor = in_data;
let treeid = cursor.get_u64_le();
let objectid = cursor.get_u64_le();
let subvol = if treeid == 0 {
current_subvol
} else {
SubvolId(treeid)
};
let path = match fs.ino_lookup(subvol, objectid).await {
Ok(Some(p)) => p,
Ok(None) => return IoctlOutcome::Err(Errno::ENOENT),
Err(e) => {
log::warn!(
"ioctl INO_LOOKUP subvol={} objectid={objectid}: {e}",
subvol.0,
);
return IoctlOutcome::Err(Errno::EIO);
}
};
let mut buf: Vec<u8> = Vec::with_capacity(INO_LOOKUP_SIZE as usize);
buf.put_u64_le(subvol.0);
buf.put_u64_le(objectid);
let mut path_bytes = path.clone();
if !path_bytes.is_empty() {
path_bytes.push(b'/');
}
let max = 4080 - 1; let n = path_bytes.len().min(max);
buf.put_slice(&path_bytes[..n]);
buf.resize(INO_LOOKUP_SIZE as usize, 0);
debug_assert_eq!(buf.len(), INO_LOOKUP_SIZE as usize);
IoctlOutcome::Ok(buf)
}
async fn tree_search_v1(
fs: &Filesystem<File>,
current_subvol: SubvolId,
in_data: &[u8],
) -> IoctlOutcome {
if in_data.len() < SEARCH_ARGS_V1_SIZE as usize {
return IoctlOutcome::Err(Errno::EINVAL);
}
let (filter, raw_key) = match parse_search_key(in_data, current_subvol) {
Ok(v) => v,
Err(o) => return o,
};
let items = match fs.tree_search(filter, SEARCH_ARGS_V1_BUF).await {
Ok(v) => v,
Err(e) => {
log::warn!("ioctl TREE_SEARCH tree={} failed: {e}", filter.tree_id);
return IoctlOutcome::Err(Errno::EIO);
}
};
let mut out: Vec<u8> = Vec::with_capacity(SEARCH_ARGS_V1_SIZE as usize);
write_search_key(&mut out, filter.tree_id, &raw_key, items.len(), None);
write_search_items(&mut out, &items);
out.resize(SEARCH_ARGS_V1_SIZE as usize, 0);
IoctlOutcome::Ok(out)
}
fn tree_search_v2() -> IoctlOutcome {
IoctlOutcome::Err(Errno::ENOPROTOOPT)
}
async fn get_subvol_rootref(
fs: &Filesystem<File>,
current_subvol: SubvolId,
in_data: &[u8],
) -> IoctlOutcome {
if in_data.len() < SUBVOL_ROOTREF_SIZE as usize {
return IoctlOutcome::Err(Errno::EINVAL);
}
let min_treeid = u64::from_le_bytes(in_data[..8].try_into().unwrap());
let filter = SearchFilter {
tree_id: 1,
min_objectid: current_subvol.0,
max_objectid: current_subvol.0,
min_type: 156,
max_type: 156,
min_offset: min_treeid,
max_offset: u64::MAX,
min_transid: 0,
max_transid: u64::MAX,
#[allow(clippy::cast_possible_truncation)]
max_items: (MAX_ROOTREF_BUFFER_NUM as u32).saturating_add(1),
};
let items = match fs.tree_search(filter, usize::MAX).await {
Ok(v) => v,
Err(e) => {
log::warn!(
"ioctl GET_SUBVOL_ROOTREF subvol={}: {e}",
current_subvol.0,
);
return IoctlOutcome::Err(Errno::EIO);
}
};
let mut entries: Vec<(u64, u64)> = Vec::new();
let mut next_min_treeid = min_treeid;
for item in items
.iter()
.filter(|it| it.objectid == current_subvol.0 && it.item_type == 156)
{
if entries.len() >= MAX_ROOTREF_BUFFER_NUM {
next_min_treeid = item.offset;
break;
}
let Some(rr) = RootRef::parse(&item.data) else {
continue;
};
entries.push((item.offset, rr.dirid));
}
let mut out: Vec<u8> = Vec::with_capacity(SUBVOL_ROOTREF_SIZE as usize);
out.put_u64_le(next_min_treeid);
for (treeid, dirid) in &entries {
out.put_u64_le(*treeid);
out.put_u64_le(*dirid);
}
out.resize(8 + MAX_ROOTREF_BUFFER_NUM * 16, 0);
#[allow(clippy::cast_possible_truncation)]
out.put_u8(entries.len() as u8);
out.resize(SUBVOL_ROOTREF_SIZE as usize, 0);
debug_assert_eq!(out.len(), SUBVOL_ROOTREF_SIZE as usize);
IoctlOutcome::Ok(out)
}
struct RawSearchKey {
min_objectid: u64,
max_objectid: u64,
min_offset: u64,
max_offset: u64,
min_transid: u64,
max_transid: u64,
min_type: u32,
max_type: u32,
}
fn parse_search_key(
in_data: &[u8],
current_subvol: SubvolId,
) -> Result<(SearchFilter, RawSearchKey), IoctlOutcome> {
if in_data.len() < SEARCH_KEY_SIZE {
return Err(IoctlOutcome::Err(Errno::EINVAL));
}
let mut key = &in_data[..SEARCH_KEY_SIZE];
let tree_id = key.get_u64_le();
let min_objectid = key.get_u64_le();
let max_objectid = key.get_u64_le();
let min_offset = key.get_u64_le();
let max_offset = key.get_u64_le();
let min_transid = key.get_u64_le();
let max_transid = key.get_u64_le();
let min_type = key.get_u32_le();
let max_type = key.get_u32_le();
let nr_items = key.get_u32_le();
let filter = SearchFilter {
tree_id: if tree_id == 0 {
current_subvol.0
} else {
tree_id
},
min_objectid,
max_objectid,
min_type,
max_type,
min_offset,
max_offset,
min_transid,
max_transid,
max_items: nr_items,
};
let raw = RawSearchKey {
min_objectid,
max_objectid,
min_offset,
max_offset,
min_transid,
max_transid,
min_type,
max_type,
};
Ok((filter, raw))
}
fn write_search_key(
out: &mut Vec<u8>,
tree_id: u64,
raw: &RawSearchKey,
actual_items: usize,
buf_size_v2: Option<u64>,
) {
out.put_u64_le(tree_id);
out.put_u64_le(raw.min_objectid);
out.put_u64_le(raw.max_objectid);
out.put_u64_le(raw.min_offset);
out.put_u64_le(raw.max_offset);
out.put_u64_le(raw.min_transid);
out.put_u64_le(raw.max_transid);
out.put_u32_le(raw.min_type);
out.put_u32_le(raw.max_type);
#[allow(clippy::cast_possible_truncation)]
out.put_u32_le(actual_items as u32);
out.put_u32_le(0); for _ in 0..4 {
out.put_u64_le(0); }
if let Some(buf_size) = buf_size_v2 {
out.put_u64_le(buf_size);
debug_assert_eq!(out.len(), SEARCH_ARGS_V2_SIZE as usize);
} else {
debug_assert_eq!(out.len(), SEARCH_KEY_SIZE);
}
}
fn write_search_items(out: &mut Vec<u8>, items: &[btrfs_fs::SearchItem]) {
for item in items {
out.put_u64_le(item.transid);
out.put_u64_le(item.objectid);
out.put_u64_le(item.offset);
out.put_u32_le(item.item_type);
#[allow(clippy::cast_possible_truncation)]
out.put_u32_le(item.data.len() as u32);
out.put_slice(&item.data);
}
}