1use crate::{
18 raw::{
19 BTRFS_FIRST_FREE_OBJECTID, BTRFS_LAST_FREE_OBJECTID,
20 BTRFS_QGROUP_INFO_KEY, BTRFS_QGROUP_LIMIT_EXCL_CMPR,
21 BTRFS_QGROUP_LIMIT_KEY, BTRFS_QGROUP_LIMIT_MAX_EXCL,
22 BTRFS_QGROUP_LIMIT_MAX_RFER, BTRFS_QGROUP_LIMIT_RFER_CMPR,
23 BTRFS_QGROUP_RELATION_KEY, BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT,
24 BTRFS_QGROUP_STATUS_FLAG_ON, BTRFS_QGROUP_STATUS_FLAG_RESCAN,
25 BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE, BTRFS_QGROUP_STATUS_KEY,
26 BTRFS_QUOTA_CTL_DISABLE, BTRFS_QUOTA_CTL_ENABLE,
27 BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA, BTRFS_QUOTA_TREE_OBJECTID,
28 BTRFS_ROOT_ITEM_KEY, BTRFS_ROOT_TREE_OBJECTID, btrfs_ioc_qgroup_assign,
29 btrfs_ioc_qgroup_create, btrfs_ioc_qgroup_limit, btrfs_ioc_quota_ctl,
30 btrfs_ioc_quota_rescan, btrfs_ioc_quota_rescan_status,
31 btrfs_ioc_quota_rescan_wait, btrfs_ioctl_qgroup_assign_args,
32 btrfs_ioctl_qgroup_create_args, btrfs_ioctl_qgroup_limit_args,
33 btrfs_ioctl_quota_ctl_args, btrfs_ioctl_quota_rescan_args,
34 btrfs_qgroup_limit,
35 },
36 tree_search::{SearchKey, tree_search},
37};
38use bitflags::bitflags;
39use nix::errno::Errno;
40use std::{
41 collections::{HashMap, HashSet},
42 mem,
43 os::{fd::AsRawFd, unix::io::BorrowedFd},
44};
45
46pub fn quota_enable(fd: BorrowedFd, simple: bool) -> nix::Result<()> {
56 let cmd = if simple {
57 u64::from(BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA)
58 } else {
59 u64::from(BTRFS_QUOTA_CTL_ENABLE)
60 };
61 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
62 args.cmd = cmd;
63 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
64 Ok(())
65}
66
67pub fn quota_disable(fd: BorrowedFd) -> nix::Result<()> {
73 let mut args: btrfs_ioctl_quota_ctl_args = unsafe { mem::zeroed() };
74 args.cmd = u64::from(BTRFS_QUOTA_CTL_DISABLE);
75 unsafe { btrfs_ioc_quota_ctl(fd.as_raw_fd(), &raw mut args) }?;
76 Ok(())
77}
78
79pub fn quota_rescan(fd: BorrowedFd) -> nix::Result<()> {
91 let args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
92 unsafe { btrfs_ioc_quota_rescan(fd.as_raw_fd(), &raw const args) }?;
93 Ok(())
94}
95
96pub fn quota_rescan_wait(fd: BorrowedFd) -> nix::Result<()> {
103 unsafe { btrfs_ioc_quota_rescan_wait(fd.as_raw_fd()) }?;
104 Ok(())
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct QuotaRescanStatus {
110 pub running: bool,
112 pub progress: u64,
115}
116
117pub fn quota_rescan_status(fd: BorrowedFd) -> nix::Result<QuotaRescanStatus> {
123 let mut args: btrfs_ioctl_quota_rescan_args = unsafe { mem::zeroed() };
124 unsafe { btrfs_ioc_quota_rescan_status(fd.as_raw_fd(), &raw mut args) }?;
125 Ok(QuotaRescanStatus {
126 running: args.flags != 0,
127 progress: args.progress,
128 })
129}
130
131#[inline]
136#[must_use]
137pub fn qgroupid_level(qgroupid: u64) -> u16 {
138 (qgroupid >> 48) as u16
139}
140
141#[inline]
145#[must_use]
146pub fn qgroupid_subvolid(qgroupid: u64) -> u64 {
147 qgroupid & 0x0000_FFFF_FFFF_FFFF
148}
149
150bitflags! {
151 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
153 pub struct QgroupStatusFlags: u64 {
154 const ON = BTRFS_QGROUP_STATUS_FLAG_ON as u64;
156 const RESCAN = BTRFS_QGROUP_STATUS_FLAG_RESCAN as u64;
158 const INCONSISTENT = BTRFS_QGROUP_STATUS_FLAG_INCONSISTENT as u64;
160 const SIMPLE_MODE = BTRFS_QGROUP_STATUS_FLAG_SIMPLE_MODE as u64;
162 }
163}
164
165bitflags! {
166 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
168 pub struct QgroupLimitFlags: u64 {
169 const MAX_RFER = BTRFS_QGROUP_LIMIT_MAX_RFER as u64;
171 const MAX_EXCL = BTRFS_QGROUP_LIMIT_MAX_EXCL as u64;
173 const RFER_CMPR = BTRFS_QGROUP_LIMIT_RFER_CMPR as u64;
175 const EXCL_CMPR = BTRFS_QGROUP_LIMIT_EXCL_CMPR as u64;
177 }
178}
179
180#[derive(Debug, Clone)]
182pub struct QgroupInfo {
183 pub qgroupid: u64,
185 pub rfer: u64,
187 pub rfer_cmpr: u64,
189 pub excl: u64,
191 pub excl_cmpr: u64,
193 pub limit_flags: QgroupLimitFlags,
195 pub max_rfer: u64,
197 pub max_excl: u64,
199 pub parents: Vec<u64>,
201 pub children: Vec<u64>,
203 pub stale: bool,
206}
207
208#[derive(Debug, Clone)]
210pub struct QgroupList {
211 pub status_flags: QgroupStatusFlags,
213 pub qgroups: Vec<QgroupInfo>,
215}
216
217#[derive(Default)]
218struct QgroupEntryBuilder {
219 has_info: bool,
221 rfer: u64,
222 rfer_cmpr: u64,
223 excl: u64,
224 excl_cmpr: u64,
225 has_limit: bool,
227 limit_flags: u64,
228 max_rfer: u64,
229 max_excl: u64,
230 parents: Vec<u64>,
232 children: Vec<u64>,
233}
234
235impl QgroupEntryBuilder {
236 fn build(self, qgroupid: u64, stale: bool) -> QgroupInfo {
237 QgroupInfo {
238 qgroupid,
239 rfer: self.rfer,
240 rfer_cmpr: self.rfer_cmpr,
241 excl: self.excl,
242 excl_cmpr: self.excl_cmpr,
243 limit_flags: QgroupLimitFlags::from_bits_truncate(self.limit_flags),
244 max_rfer: if self.limit_flags
245 & u64::from(BTRFS_QGROUP_LIMIT_MAX_RFER)
246 != 0
247 {
248 self.max_rfer
249 } else {
250 u64::MAX
251 },
252 max_excl: if self.limit_flags
253 & u64::from(BTRFS_QGROUP_LIMIT_MAX_EXCL)
254 != 0
255 {
256 self.max_excl
257 } else {
258 u64::MAX
259 },
260 parents: self.parents,
261 children: self.children,
262 stale,
263 }
264 }
265}
266
267fn parse_status_flags(data: &[u8]) -> Option<u64> {
268 btrfs_disk::items::QgroupStatus::parse(data).map(|qs| qs.flags)
269}
270
271fn parse_info(builder: &mut QgroupEntryBuilder, data: &[u8]) {
272 let Some(qi) = btrfs_disk::items::QgroupInfo::parse(data) else {
273 return;
274 };
275 builder.has_info = true;
276 builder.rfer = qi.referenced;
277 builder.rfer_cmpr = qi.referenced_compressed;
278 builder.excl = qi.exclusive;
279 builder.excl_cmpr = qi.exclusive_compressed;
280}
281
282fn parse_limit(builder: &mut QgroupEntryBuilder, data: &[u8]) {
283 let Some(ql) = btrfs_disk::items::QgroupLimit::parse(data) else {
284 return;
285 };
286 builder.has_limit = true;
287 builder.limit_flags = ql.flags;
288 builder.max_rfer = ql.max_referenced;
289 builder.max_excl = ql.max_exclusive;
290}
291
292pub fn qgroup_create(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
301 let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
302 args.create = 1;
303 args.qgroupid = qgroupid;
304 unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
307 Ok(())
308}
309
310pub fn qgroup_destroy(fd: BorrowedFd, qgroupid: u64) -> nix::Result<()> {
317 let mut args: btrfs_ioctl_qgroup_create_args = unsafe { mem::zeroed() };
318 args.create = 0;
319 args.qgroupid = qgroupid;
320 unsafe { btrfs_ioc_qgroup_create(fd.as_raw_fd(), &raw const args) }?;
323 Ok(())
324}
325
326pub fn qgroup_assign(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
339 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
340 args.assign = 1;
341 args.src = src;
342 args.dst = dst;
343 let ret =
346 unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
347 Ok(ret > 0)
348}
349
350pub fn qgroup_remove(fd: BorrowedFd, src: u64, dst: u64) -> nix::Result<bool> {
361 let mut args: btrfs_ioctl_qgroup_assign_args = unsafe { mem::zeroed() };
362 args.assign = 0;
363 args.src = src;
364 args.dst = dst;
365 let ret =
368 unsafe { btrfs_ioc_qgroup_assign(fd.as_raw_fd(), &raw const args) }?;
369 Ok(ret > 0)
370}
371
372pub fn qgroup_limit(
382 fd: BorrowedFd,
383 qgroupid: u64,
384 flags: QgroupLimitFlags,
385 max_rfer: u64,
386 max_excl: u64,
387) -> nix::Result<()> {
388 let lim = btrfs_qgroup_limit {
389 flags: flags.bits(),
390 max_referenced: max_rfer,
391 max_exclusive: max_excl,
392 rsv_referenced: 0,
393 rsv_exclusive: 0,
394 };
395 let mut args: btrfs_ioctl_qgroup_limit_args = unsafe { mem::zeroed() };
396 args.qgroupid = qgroupid;
397 args.lim = lim;
398 unsafe { btrfs_ioc_qgroup_limit(fd.as_raw_fd(), &raw mut args) }?;
402 Ok(())
403}
404
405pub fn qgroup_list(fd: BorrowedFd) -> nix::Result<QgroupList> {
415 let mut builders: HashMap<u64, QgroupEntryBuilder> = HashMap::new();
417 let mut status_flags = QgroupStatusFlags::empty();
418
419 let quota_key = SearchKey {
421 tree_id: u64::from(BTRFS_QUOTA_TREE_OBJECTID),
422 min_objectid: 0,
423 max_objectid: u64::MAX,
424 min_type: BTRFS_QGROUP_STATUS_KEY,
425 max_type: BTRFS_QGROUP_RELATION_KEY,
426 min_offset: 0,
427 max_offset: u64::MAX,
428 min_transid: 0,
429 max_transid: u64::MAX,
430 };
431
432 let scan_result = tree_search(fd, quota_key, |hdr, data| {
433 match hdr.item_type {
434 t if t == BTRFS_QGROUP_STATUS_KEY => {
435 if let Some(raw) = parse_status_flags(data) {
436 status_flags = QgroupStatusFlags::from_bits_truncate(raw);
437 }
438 }
439 t if t == BTRFS_QGROUP_INFO_KEY => {
440 let entry = builders.entry(hdr.offset).or_default();
442 parse_info(entry, data);
443 }
444 t if t == BTRFS_QGROUP_LIMIT_KEY => {
445 let entry = builders.entry(hdr.offset).or_default();
447 parse_limit(entry, data);
448 }
449 t if t == BTRFS_QGROUP_RELATION_KEY => {
450 if hdr.objectid > hdr.offset {
456 let parent = hdr.objectid;
457 let child = hdr.offset;
458 builders.entry(child).or_default().parents.push(parent);
459 builders.entry(parent).or_default().children.push(child);
460 }
461 }
462 _ => {}
463 }
464 Ok(())
465 });
466
467 match scan_result {
468 Err(Errno::ENOENT) => {
469 return Ok(QgroupList {
471 status_flags: QgroupStatusFlags::empty(),
472 qgroups: Vec::new(),
473 });
474 }
475 Err(e) => return Err(e),
476 Ok(()) => {}
477 }
478
479 let existing_subvol_ids = collect_subvol_ids(fd)?;
481
482 let mut qgroups: Vec<QgroupInfo> = builders
484 .into_iter()
485 .map(|(qgroupid, builder)| {
486 let stale = if qgroupid_level(qgroupid) == 0 {
487 !existing_subvol_ids.contains(&qgroupid_subvolid(qgroupid))
488 } else {
489 false
490 };
491 builder.build(qgroupid, stale)
492 })
493 .collect();
494
495 qgroups.sort_by_key(|q| q.qgroupid);
496
497 Ok(QgroupList {
498 status_flags,
499 qgroups,
500 })
501}
502
503fn collect_subvol_ids(fd: BorrowedFd) -> nix::Result<HashSet<u64>> {
506 let mut ids: HashSet<u64> = HashSet::new();
507
508 #[allow(clippy::cast_sign_loss)]
511 let key = SearchKey::for_objectid_range(
512 u64::from(BTRFS_ROOT_TREE_OBJECTID),
513 BTRFS_ROOT_ITEM_KEY,
514 u64::from(BTRFS_FIRST_FREE_OBJECTID),
515 BTRFS_LAST_FREE_OBJECTID as u64,
516 );
517
518 tree_search(fd, key, |hdr, _data| {
519 ids.insert(hdr.objectid);
520 Ok(())
521 })?;
522
523 Ok(ids)
524}
525
526pub fn qgroup_clear_stale(fd: BorrowedFd) -> nix::Result<usize> {
539 let list = qgroup_list(fd)?;
540 let simple_mode =
541 list.status_flags.contains(QgroupStatusFlags::SIMPLE_MODE);
542
543 let mut count = 0usize;
544
545 for qg in &list.qgroups {
546 if qgroupid_level(qg.qgroupid) != 0 || !qg.stale {
548 continue;
549 }
550
551 if simple_mode && (qg.rfer != 0 || qg.excl != 0) {
553 continue;
554 }
555
556 if qgroup_destroy(fd, qg.qgroupid).is_ok() {
557 count += 1;
558 }
559 }
560
561 Ok(count)
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn qgroupid_level_zero() {
570 assert_eq!(qgroupid_level(5), 0);
571 assert_eq!(qgroupid_level(256), 0);
572 }
573
574 #[test]
575 fn qgroupid_level_nonzero() {
576 let id = (1u64 << 48) | 100;
577 assert_eq!(qgroupid_level(id), 1);
578
579 let id = (3u64 << 48) | 42;
580 assert_eq!(qgroupid_level(id), 3);
581 }
582
583 #[test]
584 fn qgroupid_subvolid_extracts_lower_48_bits() {
585 assert_eq!(qgroupid_subvolid(256), 256);
586 assert_eq!(qgroupid_subvolid((1u64 << 48) | 100), 100);
587 assert_eq!(qgroupid_subvolid((2u64 << 48) | 0), 0);
588 }
589
590 #[test]
591 fn qgroupid_roundtrip() {
592 let level: u64 = 2;
593 let subvolid: u64 = 999;
594 let packed = (level << 48) | subvolid;
595 assert_eq!(qgroupid_level(packed), level as u16);
596 assert_eq!(qgroupid_subvolid(packed), subvolid);
597 }
598}