use crate::{
raw::{
BTRFS_FIRST_FREE_OBJECTID, BTRFS_LAST_FREE_OBJECTID,
BTRFS_QGROUP_INFO_KEY, BTRFS_QGROUP_LIMIT_EXCL_CMPR,
BTRFS_QGROUP_LIMIT_KEY, BTRFS_QGROUP_LIMIT_MAX_EXCL,
BTRFS_QGROUP_LIMIT_MAX_RFER, BTRFS_QGROUP_LIMIT_RFER_CMPR,
BTRFS_QGROUP_RELATION_KEY, BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT,
BTRFS_QGROUP_STATUS_FLAG_ON, BTRFS_QGROUP_STATUS_FLAG_RESCAN,
BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE, BTRFS_QGROUP_STATUS_KEY,
BTRFS_QUOTA_CTL_DISABLE, BTRFS_QUOTA_CTL_ENABLE,
BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA, BTRFS_QUOTA_TREE_OBJECTID,
BTRFS_ROOT_ITEM_KEY, BTRFS_ROOT_TREE_OBJECTID, btrfs_ioc_qgroup_assign,
btrfs_ioc_qgroup_create, btrfs_ioc_qgroup_limit, btrfs_ioc_quota_ctl,
btrfs_ioc_quota_rescan, btrfs_ioc_quota_rescan_status,
btrfs_ioc_quota_rescan_wait, btrfs_ioctl_qgroup_assign_args,
btrfs_ioctl_qgroup_create_args, btrfs_ioctl_qgroup_limit_args,
btrfs_ioctl_quota_ctl_args, btrfs_ioctl_quota_rescan_args,
btrfs_qgroup_limit,
},
tree_search::{Key, SearchFilter, tree_search},
};
use bitflags::bitflags;
use nix::errno::Errno;
use std::{
collections::{HashMap, HashSet},
mem,
os::{fd::AsRawFd, unix::io::BorrowedFd},
};
pub fn quota_enable(fd: BorrowedFd, simple: bool) -> nix::Result<()> {
let cmd = if simple {
u64::from(BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA)
} else {
u64::from(BTRFS_QUOTA_CTL_ENABLE)
};
let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
args.cmd = cmd;
unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
Ok(())
}
pub fn quota_disable(fd: BorrowedFd) -> nix::Result<()> {
let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
args.cmd = u64::from(BTRFS_QUOTA_CTL_DISABLE);
unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
Ok(())
}
pub fn quota_rescan(fd: BorrowedFd) -> nix::Result<()> {
let args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
unsafe { btrfs_ioc_quota_rescan(fd.as_raw_fd(), &raw const args) }?;
Ok(())
}
pub fn quota_rescan_wait(fd: BorrowedFd) -> nix::Result<()> {
unsafe { btrfs_ioc_quota_rescan_wait(fd.as_raw_fd()) }?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuotaRescanStatus {
pub running: bool,
pub progress: u64,
}
pub fn quota_rescan_status(fd: BorrowedFd) -> nix::Result<QuotaRescanStatus> {
let mut args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
unsafe { btrfs_ioc_quota_rescan_status(fd.as_raw_fd(), &raw mut args) }?;
Ok(QuotaRescanStatus {
running: args.flags != 0,
progress: args.progress,
})
}
#[inline]
#[must_use]
pub fn qgroupid_level(qgroupid: u64) -> u16 {
(qgroupid >> 48) as u16
}
#[inline]
#[must_use]
pub fn qgroupid_subvolid(qgroupid: u64) -> u64 {
qgroupid & 0x0000_FFFF_FFFF_FFFF
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QgroupStatusFlags: u64 {
const ON = BTRFS_QGROUP_STATUS_FLAG_ON as u64;
const RESCAN = BTRFS_QGROUP_STATUS_FLAG_RESCAN as u64;
const INCONSISTENT = BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT as u64;
const SIMPLE_MODE = BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE as u64;
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QgroupLimitFlags: u64 {
const MAX_RFER = BTRFS_QGROUP_LIMIT_MAX_RFER as u64;
const MAX_EXCL = BTRFS_QGROUP_LIMIT_MAX_EXCL as u64;
const RFER_CMPR = BTRFS_QGROUP_LIMIT_RFER_CMPR as u64;
const EXCL_CMPR = BTRFS_QGROUP_LIMIT_EXCL_CMPR as u64;
}
}
#[derive(Debug, Clone)]
pub struct QgroupInfo {
pub qgroupid: u64,
pub rfer: u64,
pub rfer_cmpr: u64,
pub excl: u64,
pub excl_cmpr: u64,
pub limit_flags: QgroupLimitFlags,
pub max_rfer: u64,
pub max_excl: u64,
pub parents: Vec<u64>,
pub children: Vec<u64>,
pub stale: bool,
}
#[derive(Debug, Clone)]
pub struct QgroupList {
pub status_flags: QgroupStatusFlags,
pub qgroups: Vec<QgroupInfo>,
}
#[derive(Default)]
struct QgroupEntryBuilder {
has_info: bool,
rfer: u64,
rfer_cmpr: u64,
excl: u64,
excl_cmpr: u64,
has_limit: bool,
limit_flags: u64,
max_rfer: u64,
max_excl: u64,
parents: Vec<u64>,
children: Vec<u64>,
}
impl QgroupEntryBuilder {
fn build(self, qgroupid: u64, stale: bool) -> QgroupInfo {
QgroupInfo {
qgroupid,
rfer: self.rfer,
rfer_cmpr: self.rfer_cmpr,
excl: self.excl,
excl_cmpr: self.excl_cmpr,
limit_flags: QgroupLimitFlags::from_bits_truncate(self.limit_flags),
max_rfer: if self.limit_flags
& u64::from(BTRFS_QGROUP_LIMIT_MAX_RFER)
!= 0
{
self.max_rfer
} else {
u64::MAX
},
max_excl: if self.limit_flags
& u64::from(BTRFS_QGROUP_LIMIT_MAX_EXCL)
!= 0
{
self.max_excl
} else {
u64::MAX
},
parents: self.parents,
children: self.children,
stale,
}
}
}
fn parse_status_flags(data: &[u8]) -> Option<u64> {
btrfs_disk::items::QgroupStatus::parse(data).map(|qs| qs.flags)
}
fn parse_info(builder: &mut QgroupEntryBuilder, data: &[u8]) {
let Some(qi) = btrfs_disk::items::QgroupInfo::parse(data) else {
return;
};
builder.has_info = true;
builder.rfer = qi.referenced;
builder.rfer_cmpr = qi.referenced_compressed;
builder.excl = qi.exclusive;
builder.excl_cmpr = qi.exclusive_compressed;
}
fn parse_limit(builder: &mut QgroupEntryBuilder, data: &[u8]) {
let Some(ql) = btrfs_disk::items::QgroupLimit::parse(data) else {
return;
};
builder.has_limit = true;
builder.limit_flags = ql.flags;
builder.max_rfer = ql.max_referenced;
builder.max_excl = ql.max_exclusive;
}
pub fn qgroup_create(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
args.create = 1;
args.qgroupid = qgroupid;
unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
Ok(())
}
pub fn qgroup_destroy(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
args.create = 0;
args.qgroupid = qgroupid;
unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
Ok(())
}
pub fn qgroup_assign(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
args.assign = 1;
args.src = src;
args.dst = dst;
let ret =
unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
Ok(ret > 0)
}
pub fn qgroup_remove(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
args.assign = 0;
args.src = src;
args.dst = dst;
let ret =
unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
Ok(ret > 0)
}
pub fn qgroup_limit(
fd: BorrowedFd,
qgroupid: u64,
flags: QgroupLimitFlags,
max_rfer: u64,
max_excl: u64,
) -> nix::Result<()> {
let lim = btrfs_qgroup_limit {
flags: flags.bits(),
max_referenced: max_rfer,
max_exclusive: max_excl,
rsv_referenced: 0,
rsv_exclusive: 0,
};
let mut args: btrfs_ioctl_qgroup_limit_args = unsafe { mem::zeroed() };
args.qgroupid = qgroupid;
args.lim = lim;
unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &raw mut args) }?;
Ok(())
}
pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
let mut status_flags = QgroupStatusFlags::empty();
let quota_key = SearchFilter {
tree_id: u64::from(BTRFS_QUOTA_TREE_OBJECTID),
start: Key {
objectid: 0,
item_type: BTRFS_QGROUP_STATUS_KEY,
offset: 0,
},
end: Key {
objectid: u64::MAX,
item_type: BTRFS_QGROUP_RELATION_KEY,
offset: u64::MAX,
},
min_transid: 0,
max_transid: u64::MAX,
};
let scan_result = tree_search(fd, quota_key, |hdr, data| {
match hdr.item_type {
t if t == BTRFS_QGROUP_STATUS_KEY => {
if let Some(raw) = parse_status_flags(data) {
status_flags = QgroupStatusFlags::from_bits_truncate(raw);
}
}
t if t == BTRFS_QGROUP_INFO_KEY => {
let entry = builders.entry(hdr.offset).or_default();
parse_info(entry, data);
}
t if t == BTRFS_QGROUP_LIMIT_KEY => {
let entry = builders.entry(hdr.offset).or_default();
parse_limit(entry, data);
}
t if t == BTRFS_QGROUP_RELATION_KEY => {
if hdr.objectid > hdr.offset {
let parent = hdr.objectid;
let child = hdr.offset;
builders.entry(child).or_default().parents.push(parent);
builders.entry(parent).or_default().children.push(child);
}
}
_ => {}
}
Ok(())
});
match scan_result {
Err(Errno::ENOENT) => {
return Ok(QgroupList {
status_flags: QgroupStatusFlags::empty(),
qgroups: Vec::new(),
});
}
Err(e) => return Err(e),
Ok(()) => {}
}
let existing_subvol_ids = collect_subvol_ids(fd)?;
let mut qgroups: Vec<QgroupInfo> = builders
.into_iter()
.map(|(qgroupid, builder)| {
let stale = if qgroupid_level(qgroupid) == 0 {
!existing_subvol_ids.contains(&qgroupid_subvolid(qgroupid))
} else {
false
};
builder.build(qgroupid, stale)
})
.collect();
qgroups.sort_by_key(|q| q.qgroupid);
Ok(QgroupList {
status_flags,
qgroups,
})
}
fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
let mut ids: HashSet<u64> = HashSet::new();
#[allow(clippy::cast_sign_loss)]
let key = SearchFilter::for_objectid_range(
u64::from(BTRFS_ROOT_TREE_OBJECTID),
BTRFS_ROOT_ITEM_KEY,
u64::from(BTRFS_FIRST_FREE_OBJECTID),
BTRFS_LAST_FREE_OBJECTID as u64,
);
tree_search(fd, key, |hdr, _data| {
ids.insert(hdr.objectid);
Ok(())
})?;
Ok(ids)
}
pub fn qgroup_clear_stale(fd: BorrowedFd) -> nix::Result<usize> {
let list = qgroup_list(fd)?;
let simple_mode =
list.status_flags.contains(QgroupStatusFlags::SIMPLE_MODE);
let mut count = 0usize;
for qg in &list.qgroups {
if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
continue;
}
if simple_mode && (qg.rfer != 0 || qg.excl != 0) {
continue;
}
if qgroup_destroy(fd, qg.qgroupid).is_ok() {
count += 1;
}
}
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qgroupid_level_zero() {
assert_eq!(qgroupid_level(5), 0);
assert_eq!(qgroupid_level(256), 0);
}
#[test]
fn qgroupid_level_nonzero() {
let id = (1u64 << 48) | 100;
assert_eq!(qgroupid_level(id), 1);
let id = (3u64 << 48) | 42;
assert_eq!(qgroupid_level(id), 3);
}
#[test]
fn qgroupid_subvolid_extracts_lower_48_bits() {
assert_eq!(qgroupid_subvolid(256), 256);
assert_eq!(qgroupid_subvolid((1u64 << 48) | 100), 100);
assert_eq!(qgroupid_subvolid((2u64 << 48) | 0), 0);
}
#[test]
fn qgroupid_roundtrip() {
let level: u64 = 2;
let subvolid: u64 = 999;
let packed = (level << 48) | subvolid;
assert_eq!(qgroupid_level(packed), level as u16);
assert_eq!(qgroupid_subvolid(packed), subvolid);
}
}